Attached is a first working version of the SearchBar extension, with
incremental
search and word completion.
What I need now is beta testers! Please take the 2 minutes it takes to
install this, and report and issues or comments.
(You can always disable the extension in config-extensions.def (enable=0),
and the good old dialogs will kick in.)
* AFAIK this -should- work with all recent versions of IDLE (those shipped
with Python2.2 and above).
Enjoy!
Comments et. al. much appreciated.
- Tal
Usage:
Just search as usual. When the extension is enabled it binds to the usual
find/replace events.
To complete words in the find and replace entries: Alt+/ ("slash", on the
question mark key)
Installation:
Place the attached files in your idlelib dir,
and add the following lines to config-extensions.def:
"
[SearchBar]
enable=1
is_incremental=1
[Searchbar_bindings]
toggle-search-bar=
"
(yes, the last 2 lines are required)
On 12/8/06, Tal Einat <[EMAIL PROTECTED]> wrote:
On 12/7/06, Michael Foord <[EMAIL PROTECTED]> wrote:
>
> Kurt B. Kaiser wrote:
> > My top items:
> [snip]
>
> Incremental search (in a text entry widget in the bottom status bar).
> >
>
> +1 million. :-)
>
> Not because I need it, but just because it's the best way.
I've been working some on that too. I've created a Search Bar extension
which can be used instead of the Find & Replace dialogs. The search bar
shows a thin bar on the bottom of the window, with an entry box and the
usual options (wrap, direction, case-sensitive...). For replacing, the bar
is twice as thick, with two entries and extra replacement options (replace,
find next, replace all).
"""SearchBar.py - An IDLE extension for searching for text in windows.
The interface is a small bar which appears on the bottom of the window,
and dissapears when the user stops searching.
This extension implements the usual search options, as well as regular
expressions.
Another nice feature is that while searching all matches are highlighted.
"""
import time
import string
import re
import Tkinter
from Tkconstants import TOP, BOTTOM, LEFT, RIGHT, X, NONE
from configHandler import idleConf
import SearchEngine
import WindowSearchEngine
##class FindBar:
#### menudefs = [('options', [('!Findbar', '<<toggle-findbar>>')])]
##
## def __init__(self, editwin):
## self.editwin = editwin
## self.text = editwin.text
##
## # Initialize the search manager
## mark_fg = idleConf.GetOption("extensions", "FindBar",
## "mark_fg", type="str", default="red")
## self.window_engine = FindBarSearchManager(self.text,
## mark_fg=mark_fg)
##
## self.findbar = FindBarWidget(self.editwin, self.window_engine)
## self.replacebar = FindBarWidget(self.editwin, self.window_engine,
## is_replace=True)
##
## self.enabled = False
## self.toggle_findbar_event()
##
## def toggle_findbar_event(self, event=None):
## if not self.enabled:
## self._enable()
## else:
## self._disable()
##
## def _enable(self):
## self.showid = self.text.bind("<<find>>",
## self.findbar.show_findbar_event)
#### self.againid = self.text.bind("<<find-again>>",
#### self.findbar.search_again_event)
## self.selectid = self.text.bind("<<find-selection>>",
## self.findbar.show_findbar_event)
#### self.replaceid = self.text.bind("<<replace>>",
#### self.replacebar.show_findbar_event)
## self.text.tag_configure("findsel",
## background=self.text.tag_cget("sel","background"),
## foreground=self.text.tag_cget("sel","foreground"))
##
## def _disable(self):
## self.text.unbind("<<find>>", self.showid)
#### self.text.unbind("<<find-again>>", self.againid)
## self.text.unbind("<<find-selection>>", self.selectid)
#### self.text.unbind("<<replace>>", self.replaceid)
## self.text.tag_delete("findsel")
class SearchBar:
menudefs = []
def __init__(self, editwin):
self.fb = find_bar = FindBar(editwin, editwin.status_bar)
self.rb = replace_bar = ReplaceBar(editwin, editwin.status_bar)
text = editwin.text
def find_event(event):
find_bar.show_findbar_event(event)
return "break"
text.bind("<<find>>", find_event)
def find_again_event(event):
find_bar.search_again_event(event)
return "break"
text.bind("<<find-again>>", find_again_event)
def find_selection_event(event):
find_bar.search_selection_event(event)
return "break"
text.bind("<<find-selection>>", find_selection_event)
def replace_event(event):
replace_bar.show_findbar_event(event)
return "break"
text.bind("<<replace>>", replace_event)
def FindBar(editwin, pack_after):
return SearchBarWidget(editwin, pack_after, is_replace=False)
def ReplaceBar(editwin, pack_after):
return SearchBarWidget(editwin, pack_after, is_replace=True)
class SearchBarWidget:
def __init__(self, editwin, pack_after, is_replace=False):
self.text = editwin.text
self.root = self.text._root()
self.engine = SearchEngine.get(self.root)
self.window_engine = WindowSearchEngine.get(editwin)
self.is_replace = is_replace
self.top = editwin.top
self.pack_after = pack_after
self.widgets_built = False
self.shown = False
self.find_var = Tkinter.StringVar(self.root)
# The text widget's selection isn't shown when it doesn't have the
# focus. Let's replicate it so it will be seen while searching as well.
self.text.tag_configure("findsel",
background=self.text.tag_cget("sel","background"),
foreground=self.text.tag_cget("sel","foreground"))
self._is_incremental = None
self._expand_state = None
def _show(self):
if not self.widgets_built:
self._build_widgets()
if not self.shown:
self.bar_frame.pack(side=BOTTOM, fill=X, expand=0, pady=1,
after=self.pack_after)
self.window_engine.show_find_marks()
self.shown = True # must be _before_ reset_selection()!
# Add the "findsel" tag, which looks like the selection
self._reset_selection()
self._is_incremental = self.is_incremental()
self._expand_state = None
def _hide(self):
if self.widgets_built and self.shown:
self.bar_frame.pack_forget()
self.window_engine.reset()
self.window_engine.hide_find_marks()
sel = self._get_selection()
self.shown = False # must be _after_ get_selection()!
if sel:
self._set_selection(sel[0], sel[1])
self.text.mark_set("insert", sel[0])
else:
self._reset_selection()
self.text.see("insert")
self.text.tag_remove("findsel","1.0","end")
self._is_incremental = None
self._expand_state = None
def is_incremental(self):
if self._is_incremental is None:
return idleConf.GetOption("extensions", "SearchBar",
"is_incremental", type="bool",
default=False)
else:
return self._is_incremental
def _incremental_callback(self, *args):
if self.shown and self.is_incremental():
if self.find_var.get():
self._safe_search(start=self.text.index("insert"))
else:
self.window_engine.reset()
self._clear_selection()
self.text.see("insert")
def _build_widgets(self):
if not self.widgets_built:
def _make_entry(parent, label, var):
l = Tkinter.Label(parent, text=label)
l.pack(side=LEFT, fill=NONE, expand=0)
e = Tkinter.Entry(parent, textvariable=var, exportselection=0,
width=30, border=1)
e.pack(side=LEFT, fill=NONE, expand=0)
e.bind("<Escape>", self.hide_findbar_event)
return e
def _make_checkbutton(parent, label, var):
btn = Tkinter.Checkbutton(parent, anchor="w",
text=label, variable=var)
btn.pack(side=LEFT, fill=NONE, expand=0)
btn.bind("<Escape>", self.hide_findbar_event)
return btn
def _make_button(parent, label, command):
btn = Tkinter.Button(parent, text=label, command=command)
btn.pack(side=LEFT, fill=NONE, expand=0)
btn.bind("<Escape>", self.hide_findbar_event)
return btn
# Frame for the entire bar
self.bar_frame = Tkinter.Frame(self.top, border=1, relief="flat")
# Frame for the 'Find:' / 'Replace:' entry + search options
self.find_frame = Tkinter.Frame(self.bar_frame, border=0)
# 'Find:' / 'Replace:' entry
if not self.is_replace: tmp = "Find:"
else: tmp = "Replace:"
self.find_ent = _make_entry(self.find_frame,
tmp, self.find_var)
# Regular expression checkbutton
btn = _make_checkbutton(self.find_frame,
"Reg-Exp", self.engine.revar)
if self.engine.isre():
btn.select()
self.reg_btn = btn
# Match case checkbutton
btn = _make_checkbutton(self.find_frame,
"Match case", self.engine.casevar)
if self.engine.iscase():
btn.select()
self.case_btn = btn
# Whole word checkbutton
btn = _make_checkbutton(self.find_frame,
"Whole word", self.engine.wordvar)
if self.engine.isword():
btn.select()
self.word_btn = btn
# Wrap checkbutton
btn = _make_checkbutton(self.find_frame,
"Wrap around", self.engine.wrapvar)
if self.engine.iswrap():
btn.select()
self.wrap_btn = btn
# Direction checkbutton
self.direction_txt_var = Tkinter.StringVar(self.root)
btn = Tkinter.Checkbutton(self.find_frame,
textvariable=self.direction_txt_var,
variable=self.engine.backvar,
command=self._update_direction_button,
indicatoron=0,
width=5,
)
btn.config(selectcolor=btn.cget("bg"))
btn.pack(side=RIGHT, fill=NONE, expand=0)
Tkinter.Label(self.find_frame, text="Direction:").pack(side=RIGHT,
fill=NONE,
expand=0)
if self.engine.isback():
btn.select()
self.direction_txt_var.set("Up")
else:
btn.deselect()
self.direction_txt_var.set("Down")
btn.bind("<Escape>",self.hide_findbar_event)
self.direction_btn = btn
self.find_frame.pack(side=TOP, fill=X, expand=0)
if self.is_replace:
# Frame for the 'With:' entry + replace options
self.replace_frame = Tkinter.Frame(self.bar_frame, border=0)
self.replace_with_var = Tkinter.StringVar(self.root)
self.replace_ent = _make_entry(self.replace_frame,"With:",
self.replace_with_var)
_make_button(self.replace_frame, "Find",
self._search)
_make_button(self.replace_frame, "Replace",
self._replace_event)
_make_button(self.replace_frame, "Replace All",
self._replace_all_event)
self.replace_frame.pack(side=TOP, fill=X, expand=0)
self.widgets_built = True
# Key bindings for the 'Find:' / 'Replace:' Entry widget
self.find_ent.bind("<Control-Key-f>", self._safe_search)
self.find_ent.bind("<Control-Key-g>", self._safe_search)
self.find_ent.bind("<Control-Key-R>", self._toggle_reg_event)
self.find_ent.bind("<Control-Key-C>", self._toggle_case_event)
self.find_ent.bind("<Control-Key-W>", self._toggle_wrap_event)
self.find_ent.bind("<Control-Key-D>", self._toggle_direction_event)
self.find_ent_expander = EntryExpander(self.find_ent, self.text)
self.find_ent_expander.bind("<Alt-Key-slash>")
callback = self.find_ent._register(self._incremental_callback)
self.find_ent.tk.call("trace", "variable", self.find_var, "w",
callback)
if not self.is_replace:
# Key bindings for the 'Find:' Entry widget
self.find_ent.bind("<Return>", self._safe_search)
else:
# Key bindings for the 'Replace:' Entry widget
self.find_ent.bind("<Return>", self._replace_bar_find_entry_return_event)
# Key bindings for the 'With:' Entry widget
self.replace_ent.bind("<Return>", self._replace_event)
self.replace_ent.bind("<Shift-Return>", self._safe_search)
self.replace_ent.bind("<Control-Key-f>", self._safe_search)
self.replace_ent.bind("<Control-Key-g>", self._safe_search)
self.replace_ent.bind("<Control-Key-R>", self._toggle_reg_event)
self.replace_ent.bind("<Control-Key-C>", self._toggle_case_event)
self.replace_ent.bind("<Control-Key-W>", self._toggle_wrap_event)
self.replace_ent.bind("<Control-Key-D>", self._toggle_direction_event)
self.replace_ent_expander = EntryExpander(self.replace_ent,
self.text)
self.replace_ent_expander.bind("<Alt-Key-slash>")
def _destroy_widgets(self):
if self.widgets_built:
self.bar_frame.destroy()
def show_findbar_event(self, event):
# Get the current selection
sel = self._get_selection()
if sel:
# Put the current selection in the "Find:" entry
self.find_var.set(self.text.get(sel[0],sel[1]))
# Now show the FindBar in all it's glory!
self._show()
# Set the focus to the "Find:"/"Replace:" entry
self.find_ent.focus()
# Select all of the text in the "Find:"/"Replace:" entry
self.find_ent.selection_range(0,"end")
# Hide the findbar if the focus is lost
self.bar_frame.bind("<FocusOut>", self.hide_findbar_event)
# Focus traversal (Tab or Shift-Tab) shouldn't return focus to
# the text widget
self.prev_text_takefocus_value = self.text.cget("takefocus")
self.text.config(takefocus=0)
return "break"
def hide_findbar_event(self, event=None):
self._hide()
self.text.config(takefocus=self.prev_text_takefocus_value)
self.text.focus()
return "break"
def search_again_event(self, event):
if self.engine.getpat():
return self._search(event)
else:
return self.show_findbar_event(event)
def search_selection_event(self, event):
# Get the current selection
sel = self._get_selection()
if not sel:
# No selection - beep and leave
self.text.bell()
return "break"
# Set the window's search engine's pattern to the current selection
self.find_var.set(self.text.get(sel[0],sel[1]))
return self._search(event)
def _toggle_reg_event(self, event):
self.reg_btn.invoke()
return "break"
def _toggle_case_event(self, event):
self.case_btn.invoke()
return "break"
def _toggle_wrap_event(self, event):
self.wrap_btn.invoke()
return "break"
def _toggle_direction_event(self, event):
self.direction_btn.invoke()
return "break"
def _update_direction_button(self):
if self.engine.backvar.get():
self.direction_txt_var.set("Up")
else:
self.direction_txt_var.set("Down")
def _replace_bar_find_entry_return_event(self, event=None):
# Set the focus to the "With:" entry
self.replace_ent.focus()
# Select all of the text in the "With:" entry
self.replace_ent.selection_range(0,"end")
return "break"
def _search_text(self, start, is_safe):
self.engine.patvar.set(self.find_var.get())
regexp = self.engine.getprog()
if not regexp:
return None
direction = not self.engine.isback()
wrap = self.engine.iswrap()
sel = self._get_selection()
if start is None:
if sel:
start = sel[0]
else:
start = self.text.index("insert")
if ( direction and sel and start == sel[0] and
regexp.match(self.text.get(sel[0],sel[1])) ):
_start = start + "+1c"
else:
_start = start
res = self.window_engine.findnext(regexp,
_start, direction, wrap, is_safe)
# ring the bell if the selection was found again
if sel and start == sel[0] and res == sel:
self.text.bell()
return res
def _search(self, event=None, start=None, is_safe=False):
t = time.time()
res = self._search_text(start, is_safe)
if res:
first, last = res
self._set_selection(first, last)
self.text.see(first)
if not self.shown:
self.text.mark_set("insert", first)
else:
self._clear_selection()
self.text.bell()
return "break"
def _safe_search(self, event=None, start=None):
return self._search(event=event, start=start, is_safe=True)
def _replace_event(self, event=None):
self.engine.patvar.set(self.find_var.get())
regexp = self.engine.getprog()
if not regexp:
return "break"
# Replace if appropriate
sel = self._get_selection()
if sel and regexp.match(self.text.get(sel[0], sel[1])):
replace_with = self.replace_with_var.get()
if Tkinter.tkinter.TK_VERSION >= '8.5':
# Requires at least Tk 8.5!
# This is better since the undo mechanism counts the
# replacement as one action
self.text.replace(sel[0],
sel[1],
replace_with)
else: # TK_VERSION < 8.5 - no replace method
if sel[0] != sel[1]:
self.text.delete(sel[0], sel[1])
if replace_with:
self.text.insert(sel[0], replace_with)
self.text.mark_set("insert", sel[0] + '%dc' % len(replace_with))
# Now search for the next appearance
return self._search(event, is_safe=False)
def _replace_all_event(self, event=None):
self.engine.patvar.set(self.find_var.get())
regexp = self.engine.getprog()
if not regexp:
return "break"
direction = not self.engine.isback()
wrap = self.engine.iswrap()
self.window_engine.replace_all(regexp, self.replace_with_var.get(),
direction, wrap)
return "break"
### Selection related methods
def _clear_selection(self):
tagname = self.shown and "findsel" or "sel"
self.text.tag_remove(tagname, "1.0", "end")
def _set_selection(self, start, end):
self._clear_selection()
tagname = self.shown and "findsel" or "sel"
self.text.tag_add(tagname, start, end)
def _get_selection(self):
tagname = self.shown and "findsel" or "sel"
return self.text.tag_nextrange(tagname, '1.0', 'end')
def _reset_selection(self):
if self.shown:
sel = self.text.tag_nextrange("sel", '1.0', 'end')
if sel:
self._set_selection(sel[0], sel[1])
else:
self._clear_selection()
class EntryExpander(object):
"""Expand words in an entry, taking possible words from a text widget."""
def __init__(self, entry, text):
self.text = text
self.entry = entry
self.reset()
self.entry.bind('<Map>', self.reset)
def reset(self, event=None):
self._state = None
def bind(self, event_string):
self.entry.bind(event_string, self._expand_word_event)
def _expand_word_event(self, event=None):
curinsert = self.entry.index("insert")
curline = self.entry.get()
if not self._state:
words = self._get_expand_words()
index = 0
else:
words, index, insert, line = self._state
if insert != curinsert or line != curline:
words = self._get_expand_words()
index = 0
if not words:
self.text.bell()
return "break"
curword = self._get_curr_word()
newword = words[index]
index = (index + 1) % len(words)
if index == 0:
self.text.bell() # Warn the user that we cycled around
idx = int(self.entry.index("insert"))
self.entry.delete(str(idx - len(curword)), str(idx))
self.entry.insert("insert", newword)
curinsert = self.entry.index("insert")
curline = self.entry.get()
self._state = words, index, curinsert, curline
return "break"
def _get_expand_words(self):
curword = self._get_curr_word()
if not curword:
return []
regexp = re.compile(r"\b" + curword + r"\w+\b")
# Start at 'insert wordend' so current word is first
beforewords = regexp.findall(self.text.get("1.0", "insert wordend"))
beforewords.reverse()
afterwords = regexp.findall(self.text.get("insert wordend", "end"))
# Interleave the lists of words
# (This is the next best thing to sorting by distance)
allwords = []
for a,b in zip(beforewords, afterwords):
allwords += [a,b]
minlen = len(allwords)/2
allwords += beforewords[minlen:] + afterwords[minlen:]
words_list = []
words_dict = {}
for w in allwords:
if w not in words_dict:
words_dict[w] = w
words_list.append(w)
words_list.append(curword)
return words_list
_wordchars = string.ascii_letters + string.digits + "_"
def _get_curr_word(self):
line = self.entry.get()
i = j = self.entry.index("insert")
while i > 0 and line[i-1] in self._wordchars:
i = i-1
return line[i:j]
import re
from configHandler import idleConf
import time
def get(editwin):
if not hasattr(editwin, "_window_search_engine"):
editwin._window_search_engine = WindowSearchEngine(editwin.text)
return editwin._window_search_engine
class WindowSearchEngine:
def __init__(self, text):
self.text = text
# Initialize 'findmark' tag
self.hide_find_marks()
self.reset()
def __del__(self):
self.text.tag_delete("findmark")
def show_find_marks(self):
# Get the highlight colors for 'hit'
# Do this here (and not in __init__) for color config changes to take
# effect immediately
currentTheme = idleConf.CurrentTheme()
mark_fg = idleConf.GetHighlight(currentTheme, 'hit', fgBg='fg')
mark_bg = idleConf.GetHighlight(currentTheme, 'hit', fgBg='bg')
self.text.tag_configure("findmark",
foreground=mark_fg,
background=mark_bg)
def hide_find_marks(self):
self.text.tag_configure("findmark",
foreground='',
background='')
def reset(self):
self.text.tag_remove("findmark", "1.0", "end")
self.regexp = None
def _pos2idx(self, pos):
"Convert a position in the text string to a Text widget index"
return self.text.index("1.0+%dc"%pos)
def _set_regexp(self, regexp):
"Set the current regexp; search for and mark all matches in the text"
## When searching for an extension of the previous search,
## i.e. regexp.startswith(self.regexp), update hits instead of starting from
## scratch
self.reset()
self.regexp = regexp
txt = self.text.get("1.0", "end-1c")
prev = 0
line = 1
rfind = txt.rfind
tag_add = self.text.tag_add
for res in regexp.finditer(txt):
start, end = res.span()
line += txt[prev:start].count('\n')
prev = start
start_idx = "%d.%d" % (line,
start - (rfind('\n', 0, start) + 1))
end_idx = start_idx + '+%dc'%(end-start)
tag_add("findmark", start_idx, end_idx)
def findnext(self, regexp, start, direction=1, wrap=True, is_safe=False):
"""Find the next text sequence which matches the given regexp.
The 'next' sequence is the one after the selection or the insert
cursor, or before if the direction is up instead of down.
The 'is_safe' argument tells whether it is safe to assume that the text
being searched has not been changed since the previous search; if the
text hasn't been changed then the search is almost trivial (due to
pre-processing).
"""
if regexp != self.regexp or not is_safe:
self._set_regexp(regexp)
# Search!
if direction:
next = self.text.tag_nextrange("findmark", start)
if not next and wrap:
next = self.text.tag_nextrange("findmark", '1.0', start)
else:
next = self.text.tag_prevrange("findmark", start)
if not next and wrap:
next = self.text.tag_prevrange("findmark", 'end', start)
return next
def replace_all(self, regexp, replace_with):
hit = self.findnext(regexp, '1.0',
direction=1, wrap=False, is_safe=False)
while hit:
first, last = hit
if Tkinter.tkinter.TK_VERSION >= '8.5':
# Requires at least Tk 8.5!
# This is better since the undo mechanism counts the
# replacement as one action
self.text.replace(first,
last,
replace_with)
else: # TK_VERSION < 8.5 - no replace method
if first != last:
self.text.delete(first, last)
if replace_with:
self.text.insert(first, replace_with)
hit = self.findnext(regexp, first + '%dc' % len(replace_with),
direction=1, wrap=False, is_safe=True)
def get_selection(text):
"Get the selection range in a text widget"
tmp = text.tag_nextrange("sel","1.0","end")
if tmp:
first, last = tmp
else:
first = last = text.index("insert")
return first, last
##def idx2ints(idx):
## "Convert a Text widget index to a (line, col) pair"
## line, col = map(int,idx.split(".")) # Fails on invalid index
## return line, col
##def ints2idx(ints):
## "Convert a (line, col) pair to Tk's Text widget's format."
## return "%d.%d" % ints # Fails on invalid index
_______________________________________________
IDLE-dev mailing list
[email protected]
http://mail.python.org/mailman/listinfo/idle-dev