Source code for leo.core.leoFind

#@+leo-ver=5-thin
#@+node:ekr.20060123151617: * @file leoFind.py
'''Leo's gui-independent find classes.'''
import leo.core.leoGlobals as g
import keyword
import re
import time
import sys
#@+<< Theory of operation of find/change >>
#@+node:ekr.20031218072017.2414: ** << Theory of operation of find/change >>
#@+at
#@@language rest
# 
# LeoFind.py contains the gui-independant part of all of Leo's
# find/change code. Such code is tricky, which is why it should be
# gui-independent code! Here are the governing principles:
# 
# 1. Find and Change commands initialize themselves using only the state
#    of the present Leo window. In particular, the Find class must not
#    save internal state information from one invocation to the next.
#    This means that when the user changes the nodes, or selects new
#    text in headline or body text, those changes will affect the next
#    invocation of any Find or Change command. Failure to follow this
#    principle caused all kinds of problems earlier versions.
# 
#    This principle simplifies the code because most ivars do not
#    persist. However, each command must ensure that the Leo window is
#    left in a state suitable for restarting the incremental
#    (interactive) Find and Change commands. Details of initialization
#    are discussed below.
# 
# 2. The Find and Change commands must not change the state of the
#    outline or body pane during execution. That would cause severe
#    flashing and slow down the commands a great deal. In particular,
#    c.selectPosition and c.editPosition must not be called while
#    looking for matches.
# 
# 3. When incremental Find or Change commands succeed they must leave
#    the Leo window in the proper state to execute another incremental
#    command. We restore the Leo window as it was on entry whenever an
#    incremental search fails and after any Find All and Replace All
#    command. Initialization involves setting the self.c, self.v,
#    self.in_headline, self.wrapping and self.s_text ivars.
# 
# Setting self.in_headline is tricky; we must be sure to retain the
# state of the outline pane until initialization is complete.
# Initializing the Find All and Replace All commands is much easier
# because such initialization does not depend on the state of the Leo
# window. Using the same kind of text widget for both headlines and body
# text results in a huge simplification of the code.
# 
# The searching code does not know whether it is searching headline or
# body text. The search code knows only that self.s_text is a text
# widget that contains the text to be searched or changed and the insert
# and sel attributes of self.search_text indicate the range of text to
# be searched.
# 
# Searching headline and body text simultaneously is complicated. The
# findNextMatch() method and its helpers handle the many details
# involved by setting self.s_text and its insert and sel attributes.
#@-<< Theory of operation of find/change >>
#@+others
#@+node:ekr.20070105092022.1: ** class SearchWidget
[docs]class SearchWidget(object): '''A class to simulating high-level interface widget.''' # This could be a StringTextWrapper, but this code is simple and good. def __init__(self, *args, **keys): self.s = '' # The widget text self.i = 0 # The insert point self.sel = 0, 0 # The selection range def __repr__(self): return 'SearchWidget id: %s' % (id(self)) #@+others #@+node:ekr.20070105093138: *3* getters (LeoFind)
[docs] def getAllText(self): return self.s
[docs] def getInsertPoint(self): return self.i # Returns Python index.
[docs] def getSelectionRange(self): return self.sel # Returns Python indices.
#@+node:ekr.20070105102419: *3* setters (LeoFind)
[docs] def delete(self, i, j=None): i = self.toPythonIndex(i) if j is None: j = i + 1 else: j = self.toPythonIndex(j) self.s = self.s[: i] + self.s[j:] # Bug fix: 2011/11/13: Significant in external tests. self.i = i self.sel = i, i
[docs] def insert(self, i, s): if not s: return i = self.toPythonIndex(i) self.s = self.s[: i] + s + self.s[i:] self.i = i self.sel = i, i
[docs] def setAllText(self, s): self.s = s self.i = 0 self.sel = 0, 0
[docs] def setInsertPoint(self, i, s=None): self.i = i
[docs] def setSelectionRange(self, i, j, insert=None): self.sel = self.toPythonIndex(i), self.toPythonIndex(j) if insert is not None: self.i = self.toPythonIndex(insert)
#@+node:ekr.20070105092022.4: *3* toPythonIndex (LeoFind)
[docs] def toPythonIndex(self, i): return g.toPythonIndex(self.s, i)
#@-others #@+node:ekr.20061212084717: ** class LeoFind (LeoFind.py)
[docs]class LeoFind(object): """The base class for Leo's Find commands.""" #@+others #@+node:ekr.20131117164142.17021: *3* LeoFind.birth #@+node:ekr.20031218072017.3053: *4* LeoFind.__init__ #@@nobeautify def __init__(self, c): '''Ctor for LeoFind class.''' self.c = c self.errors = 0 self.expert_mode = False # Set in finishCreate. self.ftm = None # Created by dw.createFindTab. self.frame = None self.k = c.k self.re_obj = None # Options ivars: set by FindTabManager.init. self.batch = None self.ignore_case = None self.node_only = None self.pattern_match = None self.search_headline = None self.search_body = None self.suboutline_only = None self.mark_changes = None self.mark_finds = None self.reverse = None self.wrap = None self.whole_word = None # For isearch commands... self.stack = [] # Entries are (p,sel) self.isearch_ignore_case = None self.isearch_forward = None self.isearch_regexp = None self.findTextList = [] self.changeTextList = [] # Widget ivars... self.change_ctrl = None self.s_ctrl = SearchWidget() # A helper widget for searches. self.find_text = "" self.change_text = "" self.radioButtonsChanged = False # Set by ftm.radio_button_callback # # Communication betweenfind-def and startSearch self.find_def_data = None # Saved regular find settings. self.find_seen = set() # Set of vnodes. # # Ivars containing internal state... self.buttonFlag = False self.changeAllFlag = False self.findAllFlag = False self.findAllUniqueFlag = False self.in_headline = False # True: searching headline text. self.match_obj = None # The match object returned for regex or find-all-unique-regex searches. self.p = None # The position being searched. # Never saved between searches! self.previous_find_pattern = '' # The previous find pattern, used to disable auto-setting ignore-case. self.unique_matches = set() self.was_in_headline = None # Fix bug: https://groups.google.com/d/msg/leo-editor/RAzVPihqmkI/-tgTQw0-LtwJ self.onlyPosition = None # The starting node for suboutline-only searches. self.wrapping = False # True: wrapping is enabled. # This must be different from self.wrap, which is set by the checkbox. self.wrapPosition = None # The start of wrapped searches. # Persists between calls. self.wrapPos = None # The starting position of the wrapped search. # Persists between calls. self.state_on_start_of_search = None # keeps all state data that should be restored once the search is exhausted #@+node:ekr.20150509032822.1: *4* LeoFind.cmd (decorator)
[docs] def cmd(name): '''Command decorator for the findCommands class.''' # pylint: disable=no-self-argument return g.new_cmd_decorator(name, ['c', 'findCommands',])
#@+node:ekr.20131117164142.17022: *4* LeoFind.finishCreate
[docs] def finishCreate(self): # New in 4.11.1. # Must be called when config settings are valid. c = self.c self.reloadSettings() # now that configuration settings are valid, # we can finish creating the Find pane. dw = c.frame.top if dw: dw.finishCreateLogPane()
#@+node:ekr.20171113164709.1: *4* LeoFind.reloadSettings
[docs] def reloadSettings(self): '''LeoFind.reloadSettings.''' c = self.c self.ignore_dups = c.config.getBool('find-ignore-duplicates', default=False) self.minibuffer_mode = c.config.getBool('minibuffer-find-mode', default=False)
#@+node:ekr.20060123065756.1: *3* LeoFind.Buttons (immediate execution) #@+node:ekr.20031218072017.3057: *4* find.changeAllButton
[docs] def changeAllButton(self, event=None): '''Handle Replace All button.''' c = self.c self.setup_button() c.clearAllVisited() # For context reporting. self.changeAll()
#@+node:ekr.20031218072017.3056: *4* find.changeButton
[docs] def changeButton(self, event=None): '''Handle Change button.''' self.setup_button() self.change()
#@+node:ekr.20031218072017.3058: *4* find.changeThenFindButton
[docs] def changeThenFindButton(self, event=None): '''Handle Change, Then Find button.''' self.setup_button() self.changeThenFind()
#@+node:ekr.20031218072017.3060: *4* find.findAllButton
[docs] def findAllButton(self, event=None): '''Handle Find All button.''' self.setup_button() self.findAll()
#@+node:ekr.20031218072017.3059: *4* find.findButton (headline hack)
[docs] def findButton(self, event=None): '''Handle pressing the "Find" button in the find panel.''' c, p = self.c, self.c.p p0 = p.copy() self.setup_button() # Move forward one node if we found a match in a headline: # Pressing the button destroys c.edit_widget(p), so # initInteractiveCommands does not compute ins properly. if self.was_in_headline: self.was_in_headline = False if p.hasThreadNext(): p.moveToThreadNext() c.selectPosition(p) self.p = p.copy() if not self.findNext() and p0 != c.p: # Undo the effect of selecting the next node. p0.contract() c.selectPosition(p0) c.redraw()
#@+node:ekr.20131117054619.16688: *4* find.findPreviousButton (headline hack)
[docs] def findPreviousButton(self, event=None): '''Handle the Find Previous button.''' c, p = self.c, self.c.p p0 = p.copy() self.setup_button() # Move back one node if we found a match in a headline: # Pressing the button destroys c.edit_widget(p), so # initInteractiveCommands does not compute ins properly. if self.was_in_headline: self.was_in_headline = False if p.hasThreadBack(): p.moveToThreadBack() c.selectPosition(p) self.p = p.copy() self.reverse = True try: if not self.findNext() and p0 != c.p: # Undo the effect of selecting the previous node. p0.contract() c.selectPosition(p0) c.redraw() finally: self.reverse = False
#@+node:ekr.20031218072017.3065: *4* find.setup_button
[docs] def setup_button(self): '''Init a search started by a button in the Find panel.''' c = self.c self.buttonFlag = True self.p = c.p c.bringToFront() if 0: # We _must_ retain the editing status for incremental searches! c.endEditing() self.update_ivars()
#@+node:ekr.20031218072017.3055: *3* LeoFind.Commands (immediate execution) #@+node:ekr.20031218072017.3061: *4* find.changeCommand
[docs] def changeCommand(self, event=None): '''Handle replace command.''' self.setup_command() self.change()
#@+node:ekr.20031218072017.3062: *4* find.changeThenFindCommand
[docs] @cmd('replace-then-find') def changeThenFindCommand(self, event=None): '''Handle the replace-then-find command.''' self.setup_command() self.changeThenFind()
#@+node:ekr.20131122231705.16463: *4* find.cloneFindAllCommand
[docs] def cloneFindAllCommand(self, event=None): self.setup_command() self.findAll(clone_find_all=True)
#@+node:ekr.20131122231705.16464: *4* find.cloneFindAllFlattenedCommand
[docs] def cloneFindAllFlattenedCommand(self, event=None): self.setup_command() self.findAll(clone_find_all=True, clone_find_all_flattened=True)
#@+node:ekr.20131122231705.16465: *4* find.findAllCommand
[docs] def findAllCommand(self, event=None): self.setup_command() self.findAll()
#@+node:ekr.20150629084204.1: *4* find.findDef, findVar & helpers
[docs] @cmd('find-def') def findDef(self, event=None): '''Find the def or class under the cursor.''' self.findDefHelper(event, defFlag=True)
[docs] @cmd('find-var') def findVar(self, event=None): '''Find the var under the cursor.''' self.findDefHelper(event, defFlag=False)
#@+node:ekr.20150629125733.1: *5* findDefHelper & helpers
[docs] def findDefHelper(self, event, defFlag): '''Find the definition of the class, def or var under the cursor.''' c, find, ftm = self.c, self, self.ftm w = c.frame.body.wrapper if not w: return word = self.initFindDef(event) if not word: return save_sel = w.getSelectionRange() ins = w.getInsertPoint() # For the command, always start in the root position. old_p = c.p p = c.rootPosition() # Required. c.selectPosition(p) c.redraw() c.bodyWantsFocusNow() # Set up the search. if defFlag: prefix = 'class' if word[0].isupper() else 'def' find_pattern = prefix + ' ' + word else: find_pattern = word + ' =' find.find_text = find_pattern ftm.setFindText(find_pattern) # Save previous settings. find.saveBeforeFindDef(p) find.setFindDefOptions(p) self.find_seen = set() use_cff = c.config.getBool('find-def-creates-clones', default=False) count = 0 if use_cff: count = find.findAll(clone_find_all=True, clone_find_all_flattened=True) found = count > 0 else: found = find.findNext(initFlag=False) if not found and defFlag: # Leo 5.7.3: Look for an alternative defintion of function/methods. word2 = self.switchStyle(word) if word2: find_pattern = prefix + ' ' + word2 find.find_text = find_pattern ftm.setFindText(find_pattern) if use_cff: count = find.findAll(clone_find_all=True, clone_find_all_flattened=True) found = count > 0 else: found = find.findNext(initFlag=False) if found and use_cff: last = c.lastTopLevel() if count == 1: # It's annoying to create a clone in this case. # Undo the clone find and just select the proper node. last.doDelete() find.findNext(initFlag=False) else: c.selectPosition(last) if found: self.find_seen.add(c.p.v) self.restoreAfterFindDef() # Failing to do this causes massive confusion! else: c.selectPosition(old_p) self.restoreAfterFindDef() # 2016/03/24 i, j = save_sel c.redraw() w.setSelectionRange(i, j, insert=ins) c.bodyWantsFocusNow()
#@+node:ekr.20180511045458.1: *6* switchStyle
[docs] def switchStyle(self, word): ''' Switch between camelCase and underscore_style function defintiions. Return None if there would be no change. ''' s = word if s.find('_') > -1: if s.startswith('_'): # Don't return something that looks like a class. return None # # Convert to CamelCase s = s.lower() while s: i = s.find('_') if i == -1: break s = s[:i] + s[i+1:].capitalize() return s # # Convert to underscore_style. result = [] for i, ch in enumerate(s): if i > 0 and ch.isupper(): result.append('_') result.append(ch.lower()) s = ''.join(result) return None if s == word else s
#@+node:ekr.20150629084611.1: *6* initFindDef
[docs] def initFindDef(self, event): '''Init the find-def command. Return the word to find or None.''' c = self.c w = c.frame.body.wrapper # First get the word. c.bodyWantsFocusNow() w = c.frame.body.wrapper if not w.hasSelection(): c.editCommands.extendToWord(event, select=True) word = w.getSelectedText().strip() if not word: return None if keyword.iskeyword(word): return None # Return word, stripped of preceding class or def. for tag in ('class ', 'def '): found = word.startswith(tag) and len(word) > len(tag) if found: return word[len(tag):].strip() return word
#@+node:ekr.20150629095633.1: *6* find.saveBeforeFindDef
[docs] def saveBeforeFindDef(self, p): '''Save the find settings in effect before a find-def command.''' if not self.find_def_data: self.find_def_data = g.Bunch( ignore_case = self.ignore_case, p = p.copy(), pattern_match = self.pattern_match, search_body = self.search_body, search_headline = self.search_headline, whole_word = self.whole_word, )
#@+node:ekr.20150629100600.1: *6* find.setFindDefOptions
[docs] def setFindDefOptions(self, p): '''Set the find options needed for the find-def command.''' self.ignore_case = False self.p = p.copy() self.pattern_match = False self.reverse = False self.search_body = True self.search_headline = False self.whole_word = True
#@+node:ekr.20150629095511.1: *6* find.restoreAfterFindDef
[docs] def restoreAfterFindDef(self): '''Restore find settings in effect before a find-def command.''' # pylint: disable=no-member # Bunch has these members b = self.find_def_data # A g.Bunch if b: self.ignore_case = b.ignore_case self.p = b.p self.pattern_match = b.pattern_match self.reverse = False self.search_body = b.search_body self.search_headline = b.search_headline self.whole_word = b.whole_word self.find_def_data = None
#@+node:ekr.20031218072017.3063: *4* find.findNextCommand
[docs] @cmd('find-next') def findNextCommand(self, event=None): '''The find-next command.''' self.setup_command() self.findNext()
#@+node:ekr.20031218072017.3064: *4* find.findPrevCommand
[docs] @cmd('find-prev') def findPrevCommand(self, event=None): '''Handle F2 (find-previous)''' self.setup_command() self.reverse = True try: self.findNext() finally: self.reverse = False
#@+node:ekr.20141113094129.6: *4* find.focusToFind
[docs] @cmd('focus-to-find') def focusToFind(self, event=None): c = self.c if c.config.getBool('use_find_dialog', default=True): g.app.gui.openFindDialog(c) else: c.frame.log.selectTab('Find')
#@+node:ekr.20131119204029.16479: *4* find.helpForFindCommands
[docs] def helpForFindCommands(self, event=None): '''Called from Find panel. Redirect.''' self.c.helpCommands.helpForFindCommands(event)
#@+node:ekr.20131117164142.17015: *4* find.hideFindTab
[docs] @cmd('find-tab-hide') def hideFindTab(self, event=None): '''Hide the Find tab.''' c = self.c if self.minibuffer_mode: c.k.keyboardQuit() else: self.c.frame.log.selectTab('Log')
#@+node:ekr.20131117164142.16916: *4* find.openFindTab
[docs] @cmd('find-tab-open') def openFindTab(self, event=None, show=True): '''Open the Find tab in the log pane.''' c = self.c if c.config.getBool('use_find_dialog', default=True): g.app.gui.openFindDialog(c) else: c.frame.log.selectTab('Find')
#@+node:ekr.20131117164142.17016: *4* find.changeAllCommand
[docs] def changeAllCommand(self, event=None): self.setup_command() self.changeAll() # Fixes: #722 replace-all leaves unsaved changed files c = self.c dirtyvnodes = [v for v in c.fileCommands.gnxDict.values() if v.isDirty()] def propagate(v): for v1 in v.parents: if not v1.isDirty(): v1.setDirty() propagate(v1) for v in dirtyvnodes: propagate(v) # Fix #880. c.redraw()
#@+node:ekr.20150629072547.1: *4* find.preloadFindPattern
[docs] def preloadFindPattern(self, w): '''Preload the find pattern from the selected text of widget w.''' c, ftm = self.c, self.ftm if not c.config.getBool('preload-find-pattern', default=False): # 2016/02/24: Make *sure* we don't preload the find pattern if it is not wanted. return # Enhancement #177: Use selected text as the find string. if w: if w.hasSelection(): s = self.previous_find_pattern s2 = w.getSelectedText() # Careful: Do nothing if the previous search string matches, ignoring case. # This prevents an "ignore-case" search from changing the ignore-case switch. if s.lower() != s2.lower(): ftm.setFindText(s2) # This does not work. if False and c.config.getBool('auto-set-ignore-case', default=True): mixed = s2 not in (s.lower(), s.upper()) self.ftm.set_ignore_case(not mixed) ftm.init_focus() else: c.editCommands.extendToWord(event=None, select=True, w=w) s2 = w.getSelectedText() if s2: self.find_text = s2 ftm.setFindText(s2) s = self.ftm.getFindText() self.previous_find_pattern = s
#@+node:ekr.20031218072017.3066: *4* find.setup_command # Initializes a search when a command is invoked from the menu.
[docs] def setup_command(self): if 0: # We _must_ retain the editing status for incremental searches! self.c.endEditing() # Fix bug self.buttonFlag = False self.update_ivars()
#@+node:ekr.20131119060731.22452: *4* find.startSearch
[docs] @cmd('start-search') def startSearch(self, event): w = self.editWidget(event) if w: self.preloadFindPattern(w) self.find_seen = set() if self.minibuffer_mode: self.ftm.clear_focus() self.searchWithPresentOptions(event) else: self.openFindTab(event) self.ftm.init_focus()
#@+node:vitalije.20170712162056.1: *4* find.returnToOrigin
[docs] @cmd('search-return-to-origin') def returnToOrigin(self, event): data = self.state_on_start_of_search if not data: return self.restore(data) self.restoreAllExpansionStates(data[-1], redraw=True)
#@+node:ekr.20131117164142.16939: *3* LeoFind.ISearch #@+node:ekr.20131117164142.16941: *4* find.isearchForward
[docs] @cmd('isearch-forward') def isearchForward(self, event): '''Begin a forward incremental search. - Plain characters extend the search. - !<isearch-forward>! repeats the search. - Esc or any non-plain key ends the search. - Backspace reverses the search. - Backspacing to an empty search pattern completely undoes the effect of the search. ''' self.startIncremental(event, 'isearch-forward', forward=True, ignoreCase=False, regexp=False)
#@+node:ekr.20131117164142.16942: *4* find.isearchBackward
[docs] @cmd('isearch-backward') def isearchBackward(self, event): '''Begin a backward incremental search. - Plain characters extend the search backward. - !<isearch-forward>! repeats the search. - Esc or any non-plain key ends the search. - Backspace reverses the search. - Backspacing to an empty search pattern completely undoes the effect of the search. ''' self.startIncremental(event, 'isearch-backward', forward=False, ignoreCase=False, regexp=False)
#@+node:ekr.20131117164142.16943: *4* find.isearchForwardRegexp
[docs] @cmd('isearch-forward-regexp') def isearchForwardRegexp(self, event): '''Begin a forward incremental regexp search. - Plain characters extend the search. - !<isearch-forward-regexp>! repeats the search. - Esc or any non-plain key ends the search. - Backspace reverses the search. - Backspacing to an empty search pattern completely undoes the effect of the search. ''' self.startIncremental(event, 'isearch-forward-regexp', forward=True, ignoreCase=False, regexp=True)
#@+node:ekr.20131117164142.16944: *4* find.isearchBackwardRegexp
[docs] @cmd('isearch-backward-regexp') def isearchBackwardRegexp(self, event): '''Begin a backward incremental regexp search. - Plain characters extend the search. - !<isearch-forward-regexp>! repeats the search. - Esc or any non-plain key ends the search. - Backspace reverses the search. - Backspacing to an empty search pattern completely undoes the effect of the search. ''' self.startIncremental(event, 'isearch-backward-regexp', forward=False, ignoreCase=False, regexp=True)
#@+node:ekr.20131117164142.16945: *4* find.isearchWithPresentOptions
[docs] @cmd('isearch-with-present-options') def isearchWithPresentOptions(self, event): '''Begin an incremental search using find panel options. - Plain characters extend the search. - !<isearch-forward-regexp>! repeats the search. - Esc or any non-plain key ends the search. - Backspace reverses the search. - Backspacing to an empty search pattern completely undoes the effect of the search. ''' self.startIncremental(event, 'isearch-with-present-options', forward=None, ignoreCase=None, regexp=None)
#@+node:ekr.20131117164142.16946: *3* LeoFind.Isearch utils #@+node:ekr.20131117164142.16947: *4* find.abortSearch (incremental)
[docs] def abortSearch(self): '''Restore the original position and selection.''' c = self.c; k = self.k w = c.frame.body.wrapper k.clearState() k.resetLabel() p, i, j, in_headline = self.stack[0] self.in_headline = in_headline c.selectPosition(p) c.redraw_after_select(p) c.bodyWantsFocus() w.setSelectionRange(i, j)
#@+node:ekr.20131117164142.16948: *4* find.endSearch
[docs] def endSearch(self): c, k = self.c, self.k k.clearState() k.resetLabel() c.bodyWantsFocus()
#@+node:ekr.20131117164142.16949: *4* find.iSearch
[docs] def iSearch(self, again=False): '''Handle the actual incremental search.''' c, k = self.c, self.k self.p = c.p reverse = not self.isearch_forward pattern = k.getLabel(ignorePrompt=True) if not pattern: return self.abortSearch() # Get the base ivars from the find tab. self.update_ivars() # Save oldPattern = self.find_text oldRegexp = self.pattern_match oldWord = self.whole_word # Override self.pattern_match = self.isearch_regexp self.reverse = reverse self.find_text = pattern self.whole_word = False # Word option can't be used! # Prepare the search. if len(self.stack) <= 1: self.in_headline = False w = self.setWidget() s = w.getAllText() i, j = w.getSelectionRange() if again: ins = i if reverse else j + len(pattern) else: ins = j + len(pattern) if reverse else i self.init_s_ctrl(s, ins) # Do the search! pos, newpos = self.findNextMatch() # Restore. self.find_text = oldPattern self.pattern_match = oldRegexp self.reverse = False self.whole_word = oldWord # Handle the results of the search. if pos is not None: # success. w = self.showSuccess(pos, newpos, showState=False) if w: i, j = w.getSelectionRange(sort=False) if not again: self.push(c.p, i, j, self.in_headline) elif self.wrapping: # g.es("end of wrapped search") k.setLabelRed('end of wrapped search') else: g.es("not found: %s" % (pattern)) if not again: event = g.app.gui.create_key_event(c, binding='BackSpace', char='\b', w=w) k.updateLabel(event)
#@+node:ekr.20131117164142.16950: *4* find.iSearchStateHandler
[docs] def iSearchStateHandler(self, event): '''The state manager when the state is 'isearch''' # c = self.c k = self.k stroke = event.stroke if event else None s = stroke.s if stroke else '' # No need to recognize ctrl-z. if s in ('Escape', '\n', 'Return'): self.endSearch() elif stroke in self.iSearchStrokes: self.iSearch(again=True) elif s in ('\b', 'BackSpace'): k.updateLabel(event) self.iSearchBackspace() elif( s.startswith('Ctrl+') or s.startswith('Alt+') or k.isFKey(s) # 2011/06/13. ): # End the search. self.endSearch() k.masterKeyHandler(event) # Fix bug 1267921: isearch-forward accepts non-alphanumeric keys as input. elif k.isPlainKey(stroke): k.updateLabel(event) self.iSearch()
#@+node:ekr.20131117164142.16951: *4* find.iSearchBackspace
[docs] def iSearchBackspace(self): c = self.c if len(self.stack) <= 1: self.abortSearch() return # Reduce the stack by net 1. self.pop() p, i, j, in_headline = self.pop() self.push(p, i, j, in_headline) if in_headline: # Like self.showSuccess. selection = i, j, i c.redrawAndEdit(p, selectAll=False, selection=selection, keepMinibuffer=True) else: c.selectPosition(p) w = c.frame.body.wrapper c.bodyWantsFocus() if i > j: i, j = j, i w.setSelectionRange(i, j) if len(self.stack) <= 1: self.abortSearch()
#@+node:ekr.20131117164142.16952: *4* find.getStrokes
[docs] def getStrokes(self, commandName): aList = self.inverseBindingDict.get(commandName, []) return [key for pane, key in aList]
#@+node:ekr.20131117164142.16953: *4* find.push & pop
[docs] def push(self, p, i, j, in_headline): data = p.copy(), i, j, in_headline self.stack.append(data)
[docs] def pop(self): data = self.stack.pop() p, i, j, in_headline = data return p, i, j, in_headline
#@+node:ekr.20131117164142.16954: *4* find.setWidget
[docs] def setWidget(self): c = self.c; p = c.currentPosition() wrapper = c.frame.body.wrapper if self.in_headline: w = c.edit_widget(p) if not w: # Selecting the minibuffer can kill the edit widget. selection = 0, 0, 0 c.redrawAndEdit(p, selectAll=False, selection=selection, keepMinibuffer=True) w = c.edit_widget(p) if not w: # Should never happen. g.trace('**** no edit widget!') self.in_headline = False; w = wrapper else: w = wrapper if w == wrapper: c.bodyWantsFocus() return w
#@+node:ekr.20131117164142.16955: *4* find.startIncremental
[docs] def startIncremental(self, event, commandName, forward, ignoreCase, regexp): c, k = self.c, self.k # None is a signal to get the option from the find tab. self.event = event self.isearch_forward = not self.reverse if forward is None else forward self.isearch_ignore_case = self.ignore_case if ignoreCase is None else ignoreCase self.isearch_regexp = self.pattern_match if regexp is None else regexp # Note: the word option can't be used with isearches! self.w = w = c.frame.body.wrapper self.p1 = c.p self.sel1 = w.getSelectionRange(sort=False) i, j = self.sel1 self.push(c.p, i, j, self.in_headline) self.inverseBindingDict = k.computeInverseBindingDict() self.iSearchStrokes = self.getStrokes(commandName) k.setLabelBlue('Isearch%s%s%s: ' % ( '' if self.isearch_forward else ' Backward', ' Regexp' if self.isearch_regexp else '', ' NoCase' if self.isearch_ignore_case else '', )) k.setState('isearch', 1, handler=self.iSearchStateHandler) c.minibufferWantsFocus()
#@+node:ekr.20131117164142.17013: *3* LeoFind.Minibuffer commands #@+node:ekr.20131117164142.17011: *4* find.minibufferCloneFindAll
[docs] @cmd('clone-find-all') @cmd('find-clone-all') @cmd('cfa') def minibufferCloneFindAll(self, event=None, preloaded=None): ''' clone-find-all ( aka find-clone-all and cfa). Create an organizer node whose descendants contain clones of all nodes matching the search string, except @nosearch trees. The list is *not* flattened: clones appear only once in the descendants of the organizer node. ''' w = self.editWidget(event) # sets self.w if w: if not preloaded: self.preloadFindPattern(w) self.stateZeroHelper(event, prefix='Clone Find All: ', handler=self.minibufferCloneFindAll1)
[docs] def minibufferCloneFindAll1(self, event): c, k = self.c, self.k k.clearState() k.resetLabel() k.showStateAndMode() self.generalSearchHelper(k.arg, cloneFindAll=True) c.treeWantsFocus()
#@+node:ekr.20131117164142.16996: *4* find.minibufferCloneFindAllFlattened
[docs] @cmd('clone-find-all-flattened') @cmd('find-clone-all-flattened') @cmd('cff') def minibufferCloneFindAllFlattened(self, event=None, preloaded=None): ''' clone-find-all-flattened (aka find-clone-all-flattened and cff). Create an organizer node whose direct children are clones of all nodes matching the search string, except @nosearch trees. The list is flattened: every cloned node appears as a direct child of the organizer node, even if the clone also is a descendant of another cloned node. ''' w = self.editWidget(event) # sets self.w if w: if not preloaded: self.preloadFindPattern(w) self.stateZeroHelper(event, prefix='Clone Find All Flattened: ', handler=self.minibufferCloneFindAllFlattened1)
[docs] def minibufferCloneFindAllFlattened1(self, event): c = self.c; k = self.k k.clearState() k.resetLabel() k.showStateAndMode() self.generalSearchHelper(k.arg, cloneFindAllFlattened=True) c.treeWantsFocus()
#@+node:ekr.20160920110324.1: *4* find.minibufferCloneFindTag
[docs] @cmd('clone-find-tag') @cmd('find-clone-tag') @cmd('cft') def minibufferCloneFindTag(self, event=None): ''' clone-find-tag (aka find-clone-tag and cft). Create an organizer node whose descendants contain clones of all nodes matching the given tag, except @nosearch trees. The list is *always* flattened: every cloned node appears as a direct child of the organizer node, even if the clone also is a descendant of another cloned node. ''' if self.editWidget(event): # sets self.w self.stateZeroHelper(event, prefix='Clone Find Tag: ', handler=self.minibufferCloneFindTag1)
[docs] def minibufferCloneFindTag1(self, event): c, k = self.c, self.k k.clearState() k.resetLabel() k.showStateAndMode() self.find_text = k.arg self.cloneFindTag(k.arg) c.treeWantsFocus()
#@+node:ekr.20131117164142.16998: *4* find.minibufferFindAll
[docs] @cmd('find-all') def minibufferFindAll(self, event=None): ''' Create a summary node containing descriptions of all matches of the search string. ''' self.ftm.clear_focus() self.searchWithPresentOptions(event, findAllFlag=True)
#@+node:ekr.20171226140643.1: *4* find.minibufferFindAllUnique
[docs] @cmd('find-all-unique-regex') def minibufferFindAllUniqueRegex(self, event=None): ''' Create a summary node containing all unique matches of the regex search string. This command shows only the matched string itself. ''' self.ftm.clear_focus() self.match_obj = None self.unique_matches = set() self.searchWithPresentOptions(event, findAllFlag=True, findAllUniqueFlag=True)
#@+node:ekr.20131117164142.16994: *4* find.minibufferReplaceAll
[docs] @cmd('replace-all') def minibufferReplaceAll(self, event=None): '''Replace all instances of the search string with the replacement string.''' self.ftm.clear_focus() self.searchWithPresentOptions(event, changeAllFlag=True)
#@+node:ekr.20160920164418.2: *4* find.minibufferTagChildren & helper
[docs] @cmd('tag-children') def minibufferTagChildren(self, event=None): '''tag-children: prompt for a tag and add it to all children of c.p.''' if self.editWidget(event): # sets self.w self.stateZeroHelper(event, prefix='Tag Children: ', handler=self.minibufferTagChildren1)
[docs] def minibufferTagChildren1(self, event): c, k = self.c, self.k k.clearState() k.resetLabel() k.showStateAndMode() self.tagChildren(k.arg) c.treeWantsFocus()
#@+node:ekr.20160920164418.4: *5* find.tagChildren
[docs] def tagChildren(self, tag): '''Handle the clone-find-tag command.''' c = self.c tc = c.theTagController if tc: for p in c.p.children(): tc.add_tag(p,tag) g.es_print('Added %s tag to %s nodes' % ( tag, len(list(c.p.children())))) else: g.es_print('nodetags not active')
#@+node:ekr.20131117164142.16983: *3* LeoFind.Minibuffer utils #@+node:ekr.20131117164142.16992: *4* find.addChangeStringToLabel
[docs] def addChangeStringToLabel(self): '''Add an unprotected change string to the minibuffer label.''' c = self.c ftm = c.findCommands.ftm s = ftm.getChangeText() c.minibufferWantsFocus() while s.endswith('\n') or s.endswith('\r'): s = s[: -1] c.k.extendLabel(s, select=True, protect=False)
#@+node:ekr.20131117164142.16993: *4* find.addFindStringToLabel
[docs] def addFindStringToLabel(self, protect=True): c = self.c; k = c.k ftm = c.findCommands.ftm s = ftm.getFindText() c.minibufferWantsFocus() while s.endswith('\n') or s.endswith('\r'): s = s[: -1] k.extendLabel(s, select=True, protect=protect)
#@+node:ekr.20131117164142.16985: *4* find.editWidget
[docs] def editWidget(self, event, forceFocus=True): ''' An override of baseEditCommands.editWidget that does *not* set focus when using anything other than the tk gui. This prevents this class from caching an edit widget that is about to be deallocated. ''' c = self.c # Do not cache a pointer to a headline! # It will die when the minibuffer is selected. self.w = c.frame.body.wrapper return self.w
#@+node:ekr.20131117164142.16999: *4* find.generalChangeHelper
[docs] def generalChangeHelper(self, find_pattern, change_pattern, changeAll=False): c = self.c self.setupSearchPattern(find_pattern) self.setupChangePattern(change_pattern) if c.vim_mode and c.vimCommands: c.vimCommands.update_dot_before_search( find_pattern=find_pattern, change_pattern=change_pattern) c.widgetWantsFocusNow(self.w) self.p = c.p if changeAll: self.changeAllCommand() else: # This handles the reverse option. self.findNextCommand()
#@+node:ekr.20131117164142.17000: *4* find.generalSearchHelper
[docs] def generalSearchHelper(self, pattern, cloneFindAll=False, cloneFindAllFlattened=False, findAll=False, ): c = self.c self.setupSearchPattern(pattern) if c.vim_mode and c.vimCommands: c.vimCommands.update_dot_before_search( find_pattern=pattern, change_pattern=None) # A flag indicating not a change command. c.widgetWantsFocusNow(self.w) self.p = c.p if findAll: self.findAllCommand() elif cloneFindAll: self.cloneFindAllCommand() elif cloneFindAllFlattened: self.cloneFindAllFlattenedCommand() else: # This handles the reverse option. self.findNextCommand()
#@+node:ekr.20131117164142.17001: *4* find.lastStateHelper
[docs] def lastStateHelper(self): k = self.k k.clearState() k.resetLabel() k.showStateAndMode()
#@+node:ekr.20131117164142.17003: *4* find.reSearchBackward/Forward
[docs] @cmd('re-search-backward') def reSearchBackward(self, event): self.setupArgs(forward=False, regexp=True, word=None) self.stateZeroHelper(event, 'Regexp Search Backward:', self.reSearch1, escapes=['\t']) # The Tab Easter Egg.
[docs] @cmd('re-search-forward') def reSearchForward(self, event): self.setupArgs(forward=True, regexp=True, word=None) self.stateZeroHelper(event, prefix='Regexp Search:', handler=self.reSearch1, escapes=['\t']) # The Tab Easter Egg.
[docs] def reSearch1(self, event): k = self.k if k.getArgEscapeFlag: self.setReplaceString1(event=None) else: self.updateFindList(k.arg) self.lastStateHelper() self.generalSearchHelper(k.arg)
#@+node:ekr.20131117164142.17004: *4* find.seachForward/Backward
[docs] @cmd('search-backward') def searchBackward(self, event): self.setupArgs(forward=False, regexp=False, word=False) self.stateZeroHelper(event, prefix='Search Backward: ', handler=self.search1, escapes=['\t']) # The Tab Easter Egg.
[docs] @cmd('search-forward') def searchForward(self, event): self.setupArgs(forward=True, regexp=False, word=False) self.stateZeroHelper(event, prefix='Search: ', handler=self.search1, escapes=['\t']) # The Tab Easter Egg.
[docs] def search1(self, event): k = self.k if k.getArgEscapeFlag: # Switch to the replace command. self.setReplaceString1(event=None) else: self.updateFindList(k.arg) self.lastStateHelper() self.generalSearchHelper(k.arg)
#@+node:ekr.20131117164142.17002: *4* find.setReplaceString
[docs] @cmd('set-replace-string') def setReplaceString(self, event): '''A state handler to get the replacement string.''' prompt = 'Replace ' + ('Regex' if self.pattern_match else 'String') prefix = '%s: ' % prompt self.stateZeroHelper(event, prefix=prefix, handler=self.setReplaceString1)
[docs] def setReplaceString1(self, event): k = self.k prompt = 'Replace ' + ('Regex' if self.pattern_match else 'String') self._sString = k.arg self.updateFindList(k.arg) s = '%s: %s With: ' % (prompt, self._sString) k.setLabelBlue(s) self.addChangeStringToLabel() k.getNextArg(self.setReplaceString2)
[docs] def setReplaceString2(self, event): k = self.k self.updateChangeList(k.arg) self.lastStateHelper() self.generalChangeHelper(self._sString, k.arg, changeAll=self.changeAllFlag)
#@+node:ekr.20131117164142.17005: *4* find.searchWithPresentOptions & helpers
[docs] @cmd('set-search-string') def searchWithPresentOptions(self, event, findAllFlag=False, findAllUniqueFlag=False, changeAllFlag=False, ): '''Open the search pane and get the search string.''' # Remember the entry focus, just as when using the find pane. self.changeAllFlag = changeAllFlag self.findAllFlag = findAllFlag self.findAllUniqueFlag = findAllUniqueFlag self.ftm.set_entry_focus() escapes = ['\t'] escapes.extend(self.findEscapes()) self.stateZeroHelper(event, 'Search: ', self.searchWithPresentOptions1, escapes=escapes) # The Tab Easter Egg.
[docs] def searchWithPresentOptions1(self, event): c, k = self.c, self.k if k.getArgEscapeFlag: # 2015/06/30: Special cases for F2/F3 to the escapes if event.stroke in self.findEscapes(): command = self.escapeCommand(event) func = c.commandsDict.get(command) k.clearState() k.resetLabel() k.showStateAndMode() if func: func(event) else: return g.trace('unknown command', command) else: # Switch to the replace command. if self.findAllFlag: self.changeAllFlag = True k.getArgEscapeFlag = False self.setupSearchPattern(k.arg) self.setReplaceString1(event=None) else: self.updateFindList(k.arg) k.clearState() k.resetLabel() k.showStateAndMode() if self.findAllFlag: self.setupSearchPattern(k.arg) self.findAllCommand() else: self.generalSearchHelper(k.arg)
#@+node:ekr.20150630072025.1: *5* find.findEscapes
[docs] def findEscapes(self): '''Return the keystrokes corresponding to find-next & find-prev commands.''' d = self.c.k.computeInverseBindingDict() results = [] for command in ('find-def', 'find-next', 'find-prev', 'find-var',): aList = d.get(command, []) for data in aList: pane, stroke = data if pane.startswith('all'): results.append(stroke) return results
#@+node:ekr.20150630072552.1: *5* find.escapeCommand
[docs] def escapeCommand(self, event): '''Return the escaped command to execute.''' d = self.c.k.bindingsDict aList = d.get(event.stroke) for bi in aList: if bi.stroke == event.stroke: return bi.commandName return None
#@+node:ekr.20131117164142.17007: *4* find.stateZeroHelper
[docs] def stateZeroHelper(self, event, prefix, handler, escapes=None): c, k = self.c, self.k self.w = self.editWidget(event) if not self.w: g.trace('no self.w') return k.setLabelBlue(prefix) # New in Leo 5.2: minibuffer modes shows options in status area. if self.minibuffer_mode: self.showFindOptionsInStatusArea() elif c.config.getBool('use_find_dialog', default=True): g.app.gui.openFindDialog(c) else: c.frame.log.selectTab('Find') self.addFindStringToLabel(protect=False) if escapes is None: escapes = [] k.getArgEscapes = escapes k.getArgEscapeFlag = False # k.getArg may set this. k.get1Arg(event, handler=handler, tabList=self.findTextList, completion=True)
#@+node:ekr.20131117164142.17008: *4* find.updateChange/FindList
[docs] def updateChangeList(self, s): if s not in self.changeTextList: self.changeTextList.append(s)
[docs] def updateFindList(self, s): if s not in self.findTextList: self.findTextList.append(s)
#@+node:ekr.20131117164142.17009: *4* find.wordSearchBackward/Forward (test)
[docs] @cmd('word-search-backward') def wordSearchBackward(self, event): self.setupArgs(forward=False, regexp=False, word=True) self.stateZeroHelper(event, prefix='Word Search Backward: ', handler=self.wordSearch1)
[docs] @cmd('word-search-forward') def wordSearchForward(self, event): self.setupArgs(forward=True, regexp=False, word=True) self.stateZeroHelper(event, prefix='Word Search: ', handler=self.wordSearch1)
[docs] def wordSearch1(self, event): k = self.k self.lastStateHelper() self.generalSearchHelper(k.arg)
#@+node:ekr.20131117164142.16915: *3* LeoFind.Option commands #@+node:ekr.20131117164142.16919: *4* LeoFind.toggle-find-*-option commands
[docs] @cmd('toggle-find-collapses-nodes') def toggleFindCollapesNodes(self, event): '''Toggle the 'Collapse Nodes' checkbox in the find tab.''' c = self.c c.sparse_find = not c.sparse_find if not g.unitTesting: g.es('sparse_find', c.sparse_find)
[docs] @cmd('toggle-find-ignore-case-option') def toggleIgnoreCaseOption(self, event): '''Toggle the 'Ignore Case' checkbox in the Find tab.''' return self.toggleOption('ignore_case')
[docs] @cmd('toggle-find-mark-changes-option') def toggleMarkChangesOption(self, event): '''Toggle the 'Mark Changes' checkbox in the Find tab.''' return self.toggleOption('mark_changes')
[docs] @cmd('toggle-find-mark-finds-option') def toggleMarkFindsOption(self, event): '''Toggle the 'Mark Finds' checkbox in the Find tab.''' return self.toggleOption('mark_finds')
[docs] @cmd('toggle-find-regex-option') def toggleRegexOption(self, event): '''Toggle the 'Regexp' checkbox in the Find tab.''' return self.toggleOption('pattern_match')
[docs] @cmd('toggle-find-in-body-option') def toggleSearchBodyOption(self, event): '''Set the 'Search Body' checkbox in the Find tab.''' return self.toggleOption('search_body')
[docs] @cmd('toggle-find-in-headline-option') def toggleSearchHeadlineOption(self, event): '''Toggle the 'Search Headline' checkbox in the Find tab.''' return self.toggleOption('search_headline')
[docs] @cmd('toggle-find-word-option') def toggleWholeWordOption(self, event): '''Toggle the 'Whole Word' checkbox in the Find tab.''' return self.toggleOption('whole_word')
[docs] @cmd('toggle-find-wrap-around-option') def toggleWrapSearchOption(self, event): '''Toggle the 'Wrap Around' checkbox in the Find tab.''' return self.toggleOption('wrap')
[docs] def toggleOption(self, checkbox_name): c, fc = self.c, self.c.findCommands self.ftm.toggle_checkbox(checkbox_name) options = fc.computeFindOptionsInStatusArea() c.frame.statusLine.put(options)
#@+node:ekr.20131117164142.17019: *4* LeoFind.set-find-* commands
[docs] @cmd('set-find-everywhere') def setFindScopeEveryWhere(self, event=None): '''Set the 'Entire Outline' radio button in the Find tab.''' return self.setFindScope('entire-outline')
[docs] @cmd('set-find-node-only') def setFindScopeNodeOnly(self, event=None): '''Set the 'Node Only' radio button in the Find tab.''' return self.setFindScope('node-only')
[docs] @cmd('set-find-suboutline-only') def setFindScopeSuboutlineOnly(self, event=None): '''Set the 'Suboutline Only' radio button in the Find tab.''' return self.setFindScope('suboutline-only')
[docs] def setFindScope(self, where): '''Set the radio buttons to the given scope''' c, fc = self.c, self.c.findCommands self.ftm.set_radio_button(where) options = fc.computeFindOptionsInStatusArea() c.frame.statusLine.put(options)
#@+node:ekr.20131117164142.16989: *4* LeoFind.showFindOptions & helper
[docs] @cmd('show-find-options') def showFindOptions(self, event=None): ''' Show the present find options in the status line. This is useful for commands like search-forward that do not show the Find Panel. ''' frame = self.c.frame frame.clearStatusLine() part1, part2 = self.computeFindOptions() frame.putStatusLine(part1, bg='blue') frame.putStatusLine(part2)
#@+node:ekr.20171129205648.1: *5* LeoFind.computeFindOptions
[docs] def computeFindOptions(self): '''Return the status line as two strings.''' z = [] # Set the scope field. head = self.search_headline body = self.search_body if self.suboutline_only: scope = 'tree' elif self.node_only: scope = 'node' else: scope = 'all' # scope = self.getOption('radio-search-scope') # d = {'entire-outline':'all','suboutline-only':'tree','node-only':'node'} # scope = d.get(scope) or '' head = 'head' if head else '' body = 'body' if body else '' sep = '+' if head and body else '' part1 = '%s%s%s %s ' % (head, sep, body, scope) # Set the type field. regex = self.pattern_match if regex: z.append('regex') table = ( ('reverse', 'reverse'), ('ignore_case', 'noCase'), ('whole_word', 'word'), ('wrap', 'wrap'), ('mark_changes', 'markChg'), ('mark_finds', 'markFnd'), ) for ivar, s in table: val = getattr(self, ivar) if val: z.append(s) part2 = ' '.join(z) return part1, part2
#@+node:ekr.20131117164142.16990: *4* LeoFind.setupChangePattern
[docs] def setupChangePattern(self, pattern): self.ftm.setChangeText(pattern)
#@+node:ekr.20131117164142.16991: *4* LeoFind.setupSearchPattern
[docs] def setupSearchPattern(self, pattern): self.ftm.setFindText(pattern)
#@+node:ekr.20031218072017.3067: *3* LeoFind.Utils #@+node:ekr.20031218072017.2293: *4* find.batchChange
[docs] def batchChange(self, pos1, pos2): ''' Do a single batch change operation, updating the head or body string of p and leaving the result in s_ctrl. s_ctrl contains the found text on entry and contains the changed text on exit. pos and pos2 indicate the selection. The selection will never be empty. ''' c, u = self.c, self.c.undoer p = self.p if not p: g.trace('===== can not happen: no p.') return w = self.s_ctrl # Replace the selection with self.change_text if pos1 > pos2: pos1, pos2 = pos2, pos1 s = w.getAllText() if pos1 != pos2: w.delete(pos1, pos2) w.insert(pos1, self.change_text) # Update the selection. insert = pos1 if self.reverse else pos1 + len(self.change_text) w.setSelectionRange(insert, insert) w.setInsertPoint(insert) # Update the node s = w.getAllText() if s and s[-1] == '\n': s = s[: -1] changed = s != p.h if self.in_headline else s != p.b if changed: undoData = u.beforeChangeNodeContents(p) if self.in_headline: p.initHeadString(s) else: # Fix #456: replace-all is very slow. # p.b calls c.setBodyString, which is *very* slow. p.v.setBodyString(s) if self.mark_changes: p.setMarked() # Just calls v.setMarked. # Fix #456: replace-all is very slow. # p.setDirty if very slow. p.v.setDirty() if not c.isChanged(): c.setChanged(True) u.afterChangeNodeContents( p, 'Change Headline' if self.in_headline else 'Change Body', undoData, )
#@+node:ekr.20031218072017.3068: *4* find.change
[docs] @cmd('replace') def change(self, event=None): if self.checkArgs(): self.initInHeadline() self.changeSelection()
replace = change #@+node:ekr.20031218072017.3069: *4* find.changeAll
[docs] def changeAll(self): c = self.c; u = c.undoer; undoType = 'Replace All' current = c.p t1 = time.clock() if not self.checkArgs(): return self.initInHeadline() saveData = self.save() self.initBatchCommands() count = 0 u.beforeChangeGroup(current, undoType) # Fix bug 338172: ReplaceAll will not replace newlines # indicated as \n in target string. self.change_text = self.replaceBackSlashes(self.change_text) while 1: pos1, pos2 = self.findNextMatch() if pos1 is None: break count += 1 self.batchChange(pos1, pos2) p = c.p u.afterChangeGroup(p, undoType, reportFlag=True) t2 = time.clock() g.es('changed %s instances in %4.2f sec.' % (count, (t2-t1))) # self.find_text, self.change_text, c.recolor() c.redraw(p) self.restore(saveData)
#@+node:ekr.20031218072017.3070: *4* find.changeSelection # Replace selection with self.change_text. # If no selection, insert self.change_text at the cursor.
[docs] def changeSelection(self): c = self.c p = self.p or c.p # 2015/06/22 wrapper = c.frame.body and c.frame.body.wrapper w = c.edit_widget(p) if self.in_headline else wrapper if not w: self.in_headline = False w = wrapper if not w: return oldSel = sel = w.getSelectionRange() start, end = sel if start > end: start, end = end, start if start == end: g.es("no text selected"); return False # Replace the selection in _both_ controls. start, end = oldSel change_text = self.change_text # Perform regex substitutions of \1, \2, ...\9 in the change text. if self.pattern_match and self.match_obj: groups = self.match_obj.groups() if groups: change_text = self.makeRegexSubs(change_text, groups) # change_text = change_text.replace('\\n','\n').replace('\\t','\t') change_text = self.replaceBackSlashes(change_text) for w2 in (w, self.s_ctrl): if start != end: w2.delete(start, end) w2.insert(start, change_text) w2.setInsertPoint(start if self.reverse else start + len(change_text)) # Update the selection for the next match. w.setSelectionRange(start, start + len(change_text)) c.widgetWantsFocus(w) # No redraws here: they would destroy the headline selection. if self.mark_changes: p.setMarked() if self.in_headline: c.frame.tree.onHeadChanged(p, 'Change') else: c.frame.body.onBodyChanged('Change', oldSel=oldSel) c.frame.tree.drawIcon(p) # redraw only the icon. return True
#@+node:ekr.20060526201951: *5* makeRegexSubs
[docs] def makeRegexSubs(self, s, groups): r''' Carefully substitute group[i-1] for \i strings in s. The group strings may contain \i strings: they are *not* substituted. ''' digits = '123456789' result = []; n = len(s) i = j = 0 # s[i:j] is the text between \i markers. while j < n: k = s.find('\\', j) if k == -1 or k + 1 >= n: break j = k + 1; ch = s[j] if ch in digits: j += 1 result.append(s[i: k]) # Append up to \i i = j gn = int(ch) - 1 if gn < len(groups): result.append(groups[gn]) # Append groups[i-1] else: result.append('\\%s' % ch) # Append raw '\i' result.append(s[i:]) return ''.join(result)
#@+node:ekr.20031218072017.3071: *4* find.changeThenFind
[docs] def changeThenFind(self): if not self.checkArgs(): return self.initInHeadline() if self.changeSelection(): self.findNext(False) # don't reinitialize
#@+node:ekr.20160920114454.1: *4* find.cloneFindTag & helpers
[docs] def cloneFindTag(self, tag): '''Handle the clone-find-tag command.''' c, u = self.c, self.c.undoer tc = c.theTagController if not tc: g.es_print('nodetags not active') return clones = tc.get_tagged_nodes(tag) if clones: undoType = 'Clone Find Tag' undoData = u.beforeInsertNode(c.p) found = self.createCloneTagNodes(clones) u.afterInsertNode(found, undoType, undoData, dirtyVnodeList=[]) assert c.positionExists(found, trace=True), found c.setChanged(True) c.selectPosition(found) c.redraw() else: g.es_print('tag not found: %s' % self.find_text) return len(clones)
#@+node:ekr.20160920112617.2: *5* find.createCloneTagNodes
[docs] def createCloneTagNodes(self, clones): ''' Create a "Found Tag" node as the last node of the outline. Clone all positions in the clones set as children of found. ''' c, p = self.c, self.c.p # Create the found node. assert c.positionExists(c.lastTopLevel()), c.lastTopLevel() found = c.lastTopLevel().insertAfter() assert found assert c.positionExists(found), found found.h = 'Found Tag: %s' % self.find_text # Clone nodes as children of the found node. for p in clones: # Create the clone directly as a child of found. p2 = p.copy() n = found.numberOfChildren() p2._linkAsNthChild(found, n, adjust=False) return found
#@+node:ekr.20031218072017.3073: *4* find.findAll & helpers
[docs] def findAll(self, clone_find_all=False, clone_find_all_flattened=False): c, flatten = self.c, clone_find_all_flattened clone_find = clone_find_all or flatten if flatten: undoType = 'Clone Find All Flattened' elif clone_find_all: undoType = 'Clone Find All' else: undoType = 'Find All' if not self.checkArgs(): return self.initInHeadline() data = self.save() self.initBatchCommands() # Sets self.p and self.onlyPosition. # Init suboutline-only for clone-find-all commands # Much simpler: does not set self.p or any other state. if self.pattern_match or self.findAllUniqueFlag: ok = self.precompilePattern() if not ok: return if self.suboutline_only: p = c.p after = p.nodeAfterTree() else: # Always search the entire outline. p = c.rootPosition() after = None # Fix #292: Never collapse nodes during find-all commands. old_sparse_find = c.sparse_find try: c.sparse_find = False if clone_find: count = self.doCloneFindAll(after, data, flatten, p, undoType) else: self.p = p count = self.doFindAll(after, data, p, undoType) # c.contractAllHeadlines() finally: c.sparse_find = old_sparse_find if count: c.redraw() g.es("found", count, "matches for", self.find_text) return count
#@+node:ekr.20160422072841.1: *5* find.doCloneFindAll & helpers
[docs] def doCloneFindAll(self, after, data, flatten, p, undoType): '''Handle the clone-find-all command, from p to after.''' c, u = self.c, self.c.undoer count, found = 0, None # 535: positions are not hashable, but vnodes are. clones, skip = [], set() while p and p != after: progress = p.copy() if p.v in skip: p.moveToThreadNext() else: count = self.doCloneFindAllHelper(clones, count, flatten, p, skip) assert p != progress if clones: undoData = u.beforeInsertNode(c.p) found = self.createCloneFindAllNodes(clones, flatten) u.afterInsertNode(found, undoType, undoData, dirtyVnodeList=[]) assert c.positionExists(found, trace=True), found c.setChanged(True) c.selectPosition(found) else: self.restore(data) return count
#@+node:ekr.20141023110422.1: *6* find.createCloneFindAllNodes
[docs] def createCloneFindAllNodes(self, clones, flattened): ''' Create a "Found" node as the last node of the outline. Clone all positions in the clones set a children of found. ''' c = self.c # Create the found node. assert c.positionExists(c.lastTopLevel()), c.lastTopLevel() found = c.lastTopLevel().insertAfter() assert found assert c.positionExists(found), found found.h = 'Found:%s' % self.find_text status = self.getFindResultStatus(find_all=True) status = status.strip().lstrip('(').rstrip(')').strip() flat = 'flattened, ' if flattened else '' found.b = '# %s%s\n\n# found %s nodes' % (flat, status, len(clones)) # Clone nodes as children of the found node. for p in clones: # Create the clone directly as a child of found. p2 = p.copy() n = found.numberOfChildren() p2._linkAsNthChild(found, n, adjust=False) # Sort the clones in place, without undo. found.v.children.sort(key=lambda v: v.h.lower()) return found
#@+node:ekr.20160422071747.1: *6* find.doCloneFindAllHelper
[docs] def doCloneFindAllHelper(self, clones, count, flatten, p, skip): '''Handle the cff or cfa at node p.''' if p.is_at_ignore() or re.search(r'(^@|\n@)nosearch\b', p.b): p.moveToNodeAfterTree() return count found = self.findNextBatchMatch(p) if found: if not p in clones: clones.append(p.copy()) count += 1 if flatten: skip.add(p.v) p.moveToThreadNext() elif found: # Don't look at the node or it's descendants. for p2 in p.self_and_subtree(): skip.add(p2.v) p.moveToNodeAfterTree() else: p.moveToThreadNext() return count
#@+node:ekr.20160422073500.1: *5* find.doFindAll & helpers
[docs] def doFindAll(self, after, data, p, undoType): '''Handle the find-all command from p to after.''' c, u, w = self.c, self.c.undoer, self.s_ctrl both = self.search_body and self.search_headline count, found, result = 0, None, [] while 1: pos, newpos = self.findNextMatch() if not self.p: self.p = c.p if pos is None: break count += 1 s = w.getAllText() i, j = g.getLine(s, pos) line = s[i: j] if self.findAllUniqueFlag: m = self.match_obj if m: self.unique_matches.add(m.group(0).strip()) elif both: result.append('%s%s\n%s%s\n' % ( '-' * 20, self.p.h, "head: " if self.in_headline else "body: ", line.rstrip()+'\n')) elif self.p.isVisited(): result.append(line.rstrip()+'\n') else: result.append('%s%s\n%s' % ('-' * 20, self.p.h, line.rstrip()+'\n')) self.p.setVisited() if result or self.unique_matches: undoData = u.beforeInsertNode(c.p) if self.findAllUniqueFlag: found = self.createFindUniqueNode() count = len(list(self.unique_matches)) else: found = self.createFindAllNode(result) u.afterInsertNode(found, undoType, undoData, dirtyVnodeList=[]) c.selectPosition(found) c.setChanged(True) else: self.restore(data) return count
#@+node:ekr.20150717105329.1: *6* find.createFindAllNode
[docs] def createFindAllNode(self, result): '''Create a "Found All" node as the last node of the outline.''' c = self.c found = c.lastTopLevel().insertAfter() assert found found.h = 'Found All:%s' % self.find_text status = self.getFindResultStatus(find_all=True) status = status.strip().lstrip('(').rstrip(')').strip() found.b = '# %s\n%s' % (status, ''.join(result)) return found
#@+node:ekr.20171226143621.1: *6* find.createFindUniqueNode
[docs] def createFindUniqueNode(self): '''Create a "Found Unique" node as the last node of the outline.''' c = self.c found = c.lastTopLevel().insertAfter() assert found found.h = 'Found Unique Regex:%s' % self.find_text # status = self.getFindResultStatus(find_all=True) # status = status.strip().lstrip('(').rstrip(')').strip() # found.b = '# %s\n%s' % (status, ''.join(result)) result = sorted(self.unique_matches) found.b = '\n'.join(result) return found
#@+node:ekr.20160224141710.1: *6* find.findNextBatchMatch
[docs] def findNextBatchMatch(self, p): '''Find the next batch match at p.''' table = [] if self.search_headline: table.append(p.h) if self.search_body: table.append(p.b) for s in table: self.reverse = False pos, newpos = self.searchHelper(s, 0, len(s), self.find_text) if pos != -1: return True return False
#@+node:ekr.20031218072017.3074: *4* find.findNext & helper
[docs] def findNext(self, initFlag=True): '''Find the next instance of the pattern.''' if not self.checkArgs(): return False # for vim-mode find commands. # initFlag is False for change-then-find. if initFlag: self.initInHeadline() data = self.save() self.initInteractiveCommands() else: data = self.save() pos, newpos = self.findNextMatch() if pos is None: self.restore(data) self.showStatus(False) return False # for vim-mode find commands. else: self.showSuccess(pos, newpos) self.showStatus(True) return True # for vim-mode find commands.
#@+node:ekr.20150622095118.1: *5* find.getFindResultStatus
[docs] def getFindResultStatus(self, find_all=False): '''Return the status to be shown in the status line after a find command completes.''' status = [] if self.whole_word: status.append('word' if find_all else 'word-only') if self.ignore_case: status.append('ignore-case') if self.pattern_match: status.append('regex') if find_all: if self.search_headline: status.append('head') if self.search_body: status.append('body') else: if not self.search_headline: status.append('body-only') elif not self.search_body: status.append('headline-only') if not find_all: if self.wrapping: status.append('wrapping') if self.suboutline_only: status.append('[outline-only]') elif self.node_only: status.append('[node-only]') return ' (%s)' % ', '.join(status) if status else ''
#@+node:ekr.20031218072017.3075: *4* find.findNextMatch & helpers
[docs] def findNextMatch(self): '''Resume the search where it left off.''' c, p = self.c, self.p if not self.search_headline and not self.search_body: return None, None if not self.find_text: return None, None self.errors = 0 attempts = 0 if self.pattern_match or self.findAllUniqueFlag: ok = self.precompilePattern() if not ok: return None, None while p: pos, newpos = self.search() if self.errors: g.trace('find errors') break # Abort the search. if pos is not None: # Success. if self.mark_finds: p.setMarked() if not self.changeAllFlag: c.frame.tree.drawIcon(p) # redraw only the icon. return pos, newpos # Searching the pane failed: switch to another pane or node. if self.shouldStayInNode(p): # Switching panes is possible. Do so. self.in_headline = not self.in_headline self.initNextText() else: # Switch to the next/prev node, if possible. attempts += 1 p = self.p = self.nextNodeAfterFail(p) if p: # Found another node: select the proper pane. self.in_headline = self.firstSearchPane() self.initNextText() return None, None
#@+node:ekr.20131123071505.16468: *5* find.doWrap
[docs] def doWrap(self): '''Return the position resulting from a wrap.''' c = self.c if self.reverse: p = c.rootPosition() while p and p.hasNext(): p = p.next() p = p.lastNode() return p else: return c.rootPosition()
#@+node:ekr.20131124060912.16473: *5* find.firstSearchPane
[docs] def firstSearchPane(self): ''' Set return the value of self.in_headline indicating which pane to search first. ''' if self.search_headline and self.search_body: # Fix bug 1228458: Inconsistency between Find-forward and Find-backward. if self.reverse: return False # Search the body pane first. else: return True # Search the headline pane first. elif self.search_headline or self.search_body: # Search the only enabled pane. return self.search_headline else: g.trace('can not happen: no search enabled') return False # search the body.
#@+node:ekr.20131123132043.16477: *5* find.initNextText
[docs] def initNextText(self, ins=None): ''' Init s_ctrl when a search fails. On entry: - self.in_headline indicates what text to use. - self.reverse indicates how to set the insertion point. ''' c = self.c p = self.p or c.p s = p.h if self.in_headline else p.b w = self.s_ctrl tree = c.frame and c.frame.tree if tree and hasattr(tree, 'killEditing'): tree.killEditing() if self.reverse: i, j = w.sel if ins is None: if i is not None and j is not None and i != j: ins = min(i, j) # Fix bug https://groups.google.com/d/msg/leo-editor/RAzVPihqmkI/-tgTQw0-LtwJ # editLabelHelper now properly sets the insertion range. # elif ins in (i,j): ins = min(i,j) elif ins is None: ins = 0 self.init_s_ctrl(s, ins)
#@+node:ekr.20131123132043.16476: *5* find.nextNodeAfterFail & helper
[docs] def nextNodeAfterFail(self, p): '''Return the next node after a failed search or None.''' c = self.c # Wrapping is disabled by any limitation of screen or search. wrap = (self.wrapping and not self.node_only and not self.suboutline_only and not c.hoistStack) if wrap and not self.wrapPosition: self.wrapPosition = p.copy() self.wrapPos = 0 if self.reverse else len(p.b) # Move to the next position. p = p.threadBack() if self.reverse else p.threadNext() # Check it. if p and self.outsideSearchRange(p): return None if not p and wrap: p = self.doWrap() if not p: return None if wrap and p == self.wrapPosition: return None else: return p
#@+node:ekr.20131123071505.16465: *6* find.outsideSearchRange
[docs] def outsideSearchRange(self, p): ''' Return True if the search is about to go outside its range, assuming both the headline and body text of the present node have been searched. ''' c = self.c if not p: return True if self.node_only: return True if self.suboutline_only: if self.onlyPosition: if p != self.onlyPosition and not self.onlyPosition.isAncestorOf(p): return True else: g.trace('Can not happen: onlyPosition!', p.h) return True if c.hoistStack: bunch = c.hoistStack[-1] if not bunch.p.isAncestorOf(p): g.trace('outside hoist', p.h) g.warning('found match outside of hoisted outline') return True return False # Within range.
#@+node:ekr.20131123071505.16467: *5* find.precompilePattern
[docs] def precompilePattern(self): '''Precompile the regexp pattern if necessary.''' try: # Precompile the regexp. # pylint: disable=no-member flags = re.MULTILINE if self.ignore_case: flags |= re.IGNORECASE # Escape the search text. b, s = '\\b', self.find_text if self.whole_word: if not s.startswith(b): s = b + s if not s.endswith(b): s = s + b self.re_obj = re.compile(s, flags) return True except Exception: g.warning('invalid regular expression:', self.find_text) self.errors += 1 # Abort the search. return False
#@+node:ekr.20131124060912.16472: *5* find.shouldStayInNode
[docs] def shouldStayInNode(self, p): '''Return True if the find should simply switch panes.''' # Errors here cause the find command to fail badly. # Switch only if: # a) searching both panes and, # b) this is the first pane of the pair. # There is *no way* this can ever change. # So simple in retrospect, so difficult to see. return ( self.search_headline and self.search_body and ( (self.reverse and not self.in_headline) or (not self.reverse and self.in_headline)))
#@+node:ekr.20031218072017.3076: *4* find.resetWrap
[docs] def resetWrap(self, event=None): self.wrapPosition = None self.onlyPosition = None
#@+node:ekr.20031218072017.3077: *4* find.search & helpers
[docs] def search(self): """ Search s_ctrl for self.find_text with present options. Returns (pos, newpos) or (None,None). """ c = self.c p = self.p or c.p if (self.ignore_dups or self.find_def_data) and p.v in self.find_seen: # Don't find defs/vars multiple times. return None, None w = self.s_ctrl index = w.getInsertPoint() s = w.getAllText() if sys.platform.lower().startswith('win'): s = s.replace('\r', '') # Ignore '\r' characters, which may appear in @edit nodes. # Fixes this bug: https://groups.google.com/forum/#!topic/leo-editor/yR8eL5cZpi4 # This hack would be dangerous on MacOs: it uses '\r' instead of '\n' (!) if not s: return None, None stopindex = 0 if self.reverse else len(s) pos, newpos = self.searchHelper(s, index, stopindex, self.find_text) if self.in_headline and not self.search_headline: return None, None if not self.in_headline and not self.search_body: return None, None if pos == -1: return None, None if self.passedWrapPoint(p, pos, newpos): self.wrapPosition = None # Reset. return None, None if 0: # This doesn't work because index is always zero. # Make *sure* we move past the headline. g.trace('CHECK: index: %r in_head: %s search_head: %s' % ( index, self.in_headline, self.search_headline)) if ( self.in_headline and self.search_headline and index is not None and index in (pos, newpos) ): return None, None ins = min(pos, newpos) if self.reverse else max(pos, newpos) w.setSelectionRange(pos, newpos, insert=ins) if (self.ignore_dups or self.find_def_data): self.find_seen.add(p.v) return pos, newpos
#@+node:ekr.20060526140328: *5* passedWrapPoint
[docs] def passedWrapPoint(self, p, pos, newpos): '''Return True if the search has gone beyond the wrap point.''' return ( self.wrapping and self.wrapPosition is not None and p == self.wrapPosition and (self.reverse and pos < self.wrapPos or not self.reverse and newpos > self.wrapPos) )
#@+node:ekr.20060526081931: *5* searchHelper & allies
[docs] def searchHelper(self, s, i, j, pattern): '''Dispatch the proper search method based on settings.''' backwards = self.reverse nocase = self.ignore_case regexp = self.pattern_match or self.findAllUniqueFlag word = self.whole_word if backwards: i, j = j, i if not s[i: j] or not pattern: return -1, -1 if regexp: pos, newpos = self.regexHelper(s, i, j, pattern, backwards, nocase) elif backwards: pos, newpos = self.backwardsHelper(s, i, j, pattern, nocase, word) else: pos, newpos = self.plainHelper(s, i, j, pattern, nocase, word) return pos, newpos
#@+node:ekr.20060526092203: *6* regexHelper
[docs] def regexHelper(self, s, i, j, pattern, backwards, nocase): re_obj = self.re_obj # Use the pre-compiled object if not re_obj: g.trace('can not happen: no re_obj') return -1, -1 if backwards: # Scan to the last match using search here. last_mo = None; i = 0 while i < len(s): mo = re_obj.search(s, i, j) if not mo: break i += 1 last_mo = mo mo = last_mo else: mo = re_obj.search(s, i, j) while mo and 0 <= i <= len(s): # Bug fix: 2013/12/27: must be 0 <= i <= len(s) if mo.start() == mo.end(): if backwards: # Search backward using match instead of search. i -= 1 while 0 <= i < len(s): mo = re_obj.match(s, i, j) if mo: break i -= 1 else: i += 1; mo = re_obj.search(s, i, j) else: self.match_obj = mo return mo.start(), mo.end() self.match_obj = None return -1, -1
#@+node:ekr.20060526140744: *6* backwardsHelper debugIndices = [] #@+at # rfind(sub [,start [,end]]) # # Return the highest index in the string where substring sub is found, such that # sub is contained within s[start,end]. Optional arguments start and end are # interpreted as in slice notation. Return -1 on failure. #@@c
[docs] def backwardsHelper(self, s, i, j, pattern, nocase, word): if nocase: s = s.lower() pattern = pattern.lower() # Bug fix: 10/5/06: At last the bug is found! pattern = self.replaceBackSlashes(pattern) n = len(pattern) # Put the indices in range. Indices can get out of range # because the search code strips '\r' characters when searching @edit nodes. i = max(0, i) j = min(len(s), j) # short circuit the search: helps debugging. if s.find(pattern) == -1: return -1, -1 if word: while 1: k = s.rfind(pattern, i, j) if k == -1: return -1, -1 if self.matchWord(s, k, pattern): return k, k + n else: j = max(0, k - 1) else: k = s.rfind(pattern, i, j) if k == -1: return -1, -1 else: return k, k + n
#@+node:ekr.20060526093531: *6* plainHelper
[docs] def plainHelper(self, s, i, j, pattern, nocase, word): '''Do a plain search.''' if nocase: s = s.lower(); pattern = pattern.lower() pattern = self.replaceBackSlashes(pattern) n = len(pattern) if word: while 1: k = s.find(pattern, i, j) if k == -1: return -1, -1 elif self.matchWord(s, k, pattern): return k, k + n else: i = k + n else: k = s.find(pattern, i, j) if k == -1: return -1, -1 else: return k, k + n
#@+node:ekr.20060526140744.1: *6* matchWord
[docs] def matchWord(self, s, i, pattern): '''Do a whole-word search.''' pattern = self.replaceBackSlashes(pattern) if not s or not pattern or not g.match(s, i, pattern): return False pat1, pat2 = pattern[0], pattern[-1] n = len(pattern) ch1 = s[i - 1] if 0 <= i - 1 < len(s) else '.' ch2 = s[i + n] if 0 <= i + n < len(s) else '.' isWordPat1 = g.isWordChar(pat1) isWordPat2 = g.isWordChar(pat2) isWordCh1 = g.isWordChar(ch1) isWordCh2 = g.isWordChar(ch2) inWord = isWordPat1 and isWordCh1 or isWordPat2 and isWordCh2 return not inWord
#@+node:ekr.20070105165924: *6* replaceBackSlashes
[docs] def replaceBackSlashes(self, s): '''Carefully replace backslashes in a search pattern.''' # This is NOT the same as: # s.replace('\\n','\n').replace('\\t','\t').replace('\\\\','\\') # because there is no rescanning. i = 0 while i + 1 < len(s): if s[i] == '\\': ch = s[i + 1] if ch == '\\': s = s[: i] + s[i + 1:] # replace \\ by \ elif ch == 'n': s = s[: i] + '\n' + s[i + 2:] # replace the \n by a newline elif ch == 't': s = s[: i] + '\t' + s[i + 2:] # replace \t by a tab else: i += 1 # Skip the escaped character. i += 1 return s
#@+node:ekr.20131117164142.17006: *4* find.setupArgs
[docs] def setupArgs(self, forward=False, regexp=False, word=False): ''' Set up args for commands that force various values for commands (re-/word-/search-backward/forward) that force one or more of these values to be a spefic value. ''' if forward in (True, False): self.reverse = not forward if regexp in (True, False): self.pattern_match = regexp if word in (True, False): self.whole_word = word self.showFindOptions()
#@+node:ekr.20150615174549.1: *4* find.showFindOptionsInStatusArea & helper
[docs] def showFindOptionsInStatusArea(self): '''Show find options in the status area.''' c = self.c s = self.computeFindOptionsInStatusArea() c.frame.putStatusLine(s)
#@+node:ekr.20171129211238.1: *5* find.computeFindOptionsInStatusArea
[docs] def computeFindOptionsInStatusArea(self): c = self.c ftm = c.findCommands.ftm table = ( ('Word', ftm.check_box_whole_word), ('Ig-case', ftm.check_box_ignore_case), ('regeXp', ftm.check_box_regexp), ('Body', ftm.check_box_search_body), ('Head', ftm.check_box_search_headline), ('wrap-Around', ftm.check_box_wrap_around), ('mark-Changes', ftm.check_box_mark_changes), ('mark-Finds', ftm.check_box_mark_finds), ) result = [option for option, ivar in table if ivar.checkState()] table2 = ( ('Suboutline', ftm.radio_button_suboutline_only), ('Node', ftm.radio_button_node_only), ) for option, ivar in table2: if ivar.isChecked(): result.append('[%s]' % option) break return 'Find: %s' % ' '.join(result)
#@+node:ekr.20150619070602.1: *4* find.showStatus
[docs] def showStatus(self, found): '''Show the find status the Find dialog, if present, and the status line.''' c = self.c d = getattr(g.app.gui, 'globalFindDialog', None) status = 'found' if found else 'not found' options = self.getFindResultStatus() s = '%s:%s %s' % (status, options, self.find_text) if d: top = c.frame.top top.find_status_edit.setText(s) # Set colors. found_bg = c.config.getColor('find-found-bg') or 'blue' not_found_bg = c.config.getColor('find-not-found-bg') or 'red' found_fg = c.config.getColor('find-found-fg') or 'white' not_found_fg = c.config.getColor('find-not-found-fg') or 'white' bg = found_bg if found else not_found_bg fg = found_fg if found else not_found_fg if c.config.getBool("show-find-result-in-status") is not False: c.frame.putStatusLine(s, bg=bg, fg=fg) if not found: # Fixes: #457 self.radioButtonsChanged = True self.reset_state_ivars()
#@+node:ekr.20031218072017.3082: *3* LeoFind.Initing & finalizing #@+node:ekr.20031218072017.3083: *4* find.checkArgs
[docs] def checkArgs(self): val = True if not self.search_headline and not self.search_body: g.es("not searching headline or body") val = False s = self.ftm.getFindText() if not s: g.es("empty find patttern") val = False return val
#@+node:ekr.20131124171815.16629: *4* find.init_s_ctrl
[docs] def init_s_ctrl(self, s, ins): '''Init the contents of s_ctrl from s and ins.''' w = self.s_ctrl w.setAllText(s) if ins is None: # A flag telling us to search all of w. ins = len(s) if self.reverse else 0 w.setInsertPoint(ins)
#@+node:ekr.20031218072017.3084: *4* find.initBatchCommands (sets in_headline)
[docs] def initBatchCommands(self): '''Init for find-all and replace-all commands.''' c = self.c self.errors = 0 self.in_headline = self.search_headline # Search headlines first. # Select the first node. if self.suboutline_only or self.node_only: self.p = c.p # Fix bug 188: Find/Replace All Suboutline only same as Node only # https://github.com/leo-editor/leo-editor/issues/188 self.onlyPosition = self.p.copy() else: p = c.rootPosition() if self.reverse: while p and p.next(): p = p.next() p = p.lastNode() self.p = p # Set the insert point. self.initBatchText()
#@+node:ekr.20031218072017.3085: *4* find.initBatchText
[docs] def initBatchText(self, ins=None): '''Init s_ctrl from self.p and ins at the beginning of a search.''' c = self.c self.wrapping = False # Only interactive commands allow wrapping. p = self.p or c.p s = p.h if self.in_headline else p.b self.init_s_ctrl(s, ins)
#@+node:ekr.20031218072017.3086: *4* find.initInHeadline & helper
[docs] def initInHeadline(self): ''' Select the first pane to search for incremental searches and changes. This is called only at the start of each search. This must not alter the current insertion point or selection range. ''' # # Fix bug 1228458: Inconsistency between Find-forward and Find-backward. if self.search_headline and self.search_body: # We have no choice: we *must* search the present widget! self.in_headline = self.focusInTree() else: self.in_headline = self.search_headline
#@+node:ekr.20131126085250.16651: *5* find.focusInTree
[docs] def focusInTree(self): ''' Return True is the focus widget w is anywhere in the tree pane. Note: the focus may be in the find pane. ''' c = self.c ftm = self.ftm w = ftm.entry_focus or g.app.gui.get_focus(raw=True) ftm.entry_focus = None # Only use this focus widget once! w_name = c.widget_name(w) if self.buttonFlag and self.was_in_headline in (True, False): # Fix bug: https://groups.google.com/d/msg/leo-editor/RAzVPihqmkI/-tgTQw0-LtwJ self.in_headline = self.was_in_headline val = self.was_in_headline # Easy case: focus in body. elif w == c.frame.body.wrapper: val = False elif w == c.frame.tree.treeWidget: val = True else: val = w_name.startswith('head') return val
#@+node:ekr.20031218072017.3087: *4* find.initInteractiveCommands
[docs] def initInteractiveCommands(self): ''' Init an interactive command. This is tricky! *Always* start in the presently selected widget, provided that searching is enabled for that widget. Always start at the present insert point for the body pane. For headlines, start at beginning or end of the headline text. ''' c = self.c p = self.p = c.p # *Always* start with the present node. wrapper = c.frame.body and c.frame.body.wrapper headCtrl = c.edit_widget(p) # w is the real widget. It may not exist for headlines. w = headCtrl if self.in_headline else wrapper # We only use the insert point, *never* the selection range. # None is a signal to self.initNextText() ins = w.getInsertPoint() if w else None self.errors = 0 self.initNextText(ins=ins) if w: c.widgetWantsFocus(w) # Init suboutline-only: if self.suboutline_only and not self.onlyPosition: self.onlyPosition = p.copy() # Wrap does not apply to limited searches. if (self.wrap and not self.node_only and not self.suboutline_only and self.wrapPosition is None ): self.wrapping = True self.wrapPos = ins
# Do not set self.wrapPosition here: that must be done after the first search. #@+node:ekr.20031218072017.3088: *4* find.printLine
[docs] def printLine(self, line, allFlag=False): both = self.search_body and self.search_headline context = self.batch # "batch" now indicates context if allFlag and both and context: g.es('', '-' * 20, '', self.p.h) theType = "head: " if self.in_headline else "body: " g.es('', theType + line) elif allFlag and context and not self.p.isVisited(): # We only need to print the context once. g.es('', '-' * 20, '', self.p.h) g.es('', line) self.p.setVisited() else: g.es('', line)
#@+node:ekr.20131126174039.16719: *4* find.reset_state_ivars
[docs] def reset_state_ivars(self): '''Reset ivars related to suboutline-only and wrapped searches.''' self.onlyPosition = None self.wrapping = False self.wrapPosition = None self.wrapPos = None
#@+node:ekr.20031218072017.3089: *4* find.restore (headline hack)
[docs] def restore(self, data): '''Restore the screen and clear state after a search fails.''' c = self.c in_headline, editing, p, w, insert, start, end, junk = data self.was_in_headline = False # 2015/03/25 if 0: # Don't do this here. # Reset ivars related to suboutline-only and wrapped searches. self.reset_state_ivars() if c.config.getBool('close-find-dialog-after-search', default=True): if hasattr(g.app.gui, 'hideFindDialog'): g.app.gui.hideFindDialog() c.frame.bringToFront() # Needed on the Mac # Don't try to reedit headline. if p and c.positionExists(p): # 2013/11/22. c.selectPosition(p) else: c.selectPosition(c.rootPosition()) # New in Leo 4.5. self.restoreAfterFindDef() # Fix bug 1258373: https://bugs.launchpad.net/leo-editor/+bug/1258373 if in_headline: c.selectPosition(p) if False and editing: c.editHeadline() else: c.treeWantsFocus() else: # Looks good and provides clear indication of failure or termination. w.setSelectionRange(start, end, insert=insert) w.seeInsertPoint() c.widgetWantsFocus(w)
#@+node:vitalije.20170712102153.1: *4* find.restoreAllExpansionStates
[docs] def restoreAllExpansionStates(self, expanded, redraw=False): '''expanded is a set of gnx of nodes that should be expanded''' c = self.c; gnxDict = c.fileCommands.gnxDict for gnx, v in gnxDict.items(): if gnx in expanded: v.expand() else: v.contract() if redraw: c.redraw()
#@+node:ekr.20031218072017.3090: *4* find.save
[docs] def save(self): '''Save everything needed to restore after a search fails.''' c = self.c p = self.p or c.p # Fix bug 1258373: https://bugs.launchpad.net/leo-editor/+bug/1258373 if self.in_headline: e = c.edit_widget(p) w = e or c.frame.tree.canvas insert, start, end = None, None, None else: w = c.frame.body.wrapper e = None insert = w.getInsertPoint() sel = w.getSelectionRange() if len(sel) == 2: start, end = sel else: start, end = None, None editing = e is not None expanded = set(gnx for gnx, v in c.fileCommands.gnxDict.items() if v.isExpanded()) # TODO: this is naive solution that treat all clones the same way if one is expanded # then every other clone is expanded too. A proper way would be to remember # each clone separately return self.in_headline, editing, p.copy(), w, insert, start, end, expanded
#@+node:ekr.20031218072017.3091: *4* find.showSuccess (headline hack)
[docs] def showSuccess(self, pos, newpos, showState=True): '''Display the result of a successful find operation.''' c = self.c self.p = p = self.p or c.p # Set state vars. # Ensure progress in backwards searches. insert = min(pos, newpos) if self.reverse else max(pos, newpos) if self.wrap and not self.wrapPosition: self.wrapPosition = self.p if c.sparse_find: c.expandOnlyAncestorsOfNode(p=p) if self.in_headline: c.endEditing() selection = pos, newpos, insert c.redrawAndEdit(p, selection=selection, keepMinibuffer=True) w = c.edit_widget(p) self.was_in_headline = True # 2015/03/25 else: w = c.frame.body.wrapper # *Always* do the full selection logic. # This ensures that the body text is inited and recolored. c.selectPosition(p) c.bodyWantsFocus() if showState: c.k.showStateAndMode(w) c.bodyWantsFocusNow() w.setSelectionRange(pos, newpos, insert=insert) w.see(insert) # Fix bug 78: find-next match not always scrolled into view. # https://github.com/leo-editor/leo-editor/issues/78 g.app.allow_delayed_see = True c.outerUpdate() if c.vim_mode and c.vimCommands: c.vimCommands.update_selection_after_search() # Support for the console gui. if hasattr(g.app.gui, 'show_find_success'): g.app.gui.show_find_success(c, self.in_headline, insert, p) if c.config.getBool('close-find-dialog-after-search', default=True): if hasattr(g.app.gui, 'hideFindDialog'): g.app.gui.hideFindDialog() c.frame.bringToFront() return w # Support for isearch.
#@+node:ekr.20031218072017.1460: *4* find.update_ivars
[docs] def update_ivars(self): """Update ivars from the find panel.""" c = self.c self.p = c.p ftm = self.ftm # The caller is responsible for removing most trailing cruft. # Among other things, this allows Leo to search for a single trailing space. s = ftm.getFindText() s = g.toUnicode(s) if s and s[-1] in ('\r', '\n'): s = s[: -1] if self.radioButtonsChanged or s != self.find_text: self.radioButtonsChanged = False self.state_on_start_of_search = self.save() # Reset ivars related to suboutline-only and wrapped searches. self.reset_state_ivars() self.find_text = s # Disable part of https://github.com/leo-editor/leo-editor/issues/177 # Set ignore-case if the find text is mixed case. # This does not work well in practice. if False and c.config.getBool('auto-set-ignore-case', default=True): # Careful: Alter the ignore-case option only if the # search pattern has actually changed. if self.previous_find_pattern != s: # Only change the setting for mixed case. mixed = s not in (s.lower(), s.upper()) ftm.set_ignore_case(not mixed) # Get replacement text. s = ftm.getReplaceText() s = g.toUnicode(s) if s and s[-1] in ('\r', '\n'): s = s[: -1] self.change_text = s
#@-others #@-others #@@language python #@@tabwidth -4 #@@pagewidth 70 #@-leo