Now that the search minibuffer doesn't show the current search direction, changing...
[rox-edit.git] / search.py
blob10c26ed110b49fccf8478dc10c44eda8014bba8e
1 import rox
2 from rox import g
3 from EditWindow import Minibuffer
5 regex_help = (
6 ('.', _('Matches any character')),
7 ('[a-z]', _('Any lowercase letter')),
8 ('[-+*/]', _('Any character listed (- must be first)')),
9 ('^A', _('A only at the start of a line')),
10 ('A$', _('A only at the end of a line')),
11 ('A*', _('Zero or more A')),
12 ('A+', _('One or more A')),
13 ('A?', _('Zero or one A')),
14 ('A{m,n}', _('Between m and n matches of A')),
15 ('A*?, A+?, A??, A{}?', _('Non-greedy versions of *, +, ? and {}')),
16 ('\*, \+, etc', _('Literal "*", "+"')),
17 ('A|B', _('Can match A or B')),
18 ('(AB)', _('Group A and B together (for *, \\1, etc)')),
19 ('\\1, \\2, etc', _('The first/second bracketed match (goes in the With: box)')),
20 ('\\b', _('Word boundary (eg, \\bWord\\b)')),
21 ('\\B', _('Non-word boundary')),
22 ('\\d, \\D', _('Digit, non-digit')),
23 ('\\s, \\S', _('Whitespace, non-whitespace')),
24 (_('Others'), _('See the Python regular expression documentation for more')),
25 ('', ''),
26 (_('Examples:'), ''),
27 ('Fred', _('Matches "Fred" anywhere')),
28 ('^Fred$', _('A line containing only "Fred"')),
29 ('Go+gle', _('"Gogle", "Google", "Gooogle", etc')),
30 ('Colou?r', _('Colour or Color')),
31 ('[tT]he', _('"The" or "the"')),
32 ('M.*d', '"Md", "Mad", "Mud", "Mind", etc'),
33 ('([ab][cd])+', '"ac", "ad", "acbdad", etc'),
34 ('', ''),
35 (_('Python expressions:'), ''),
36 ('old', _('The text that was matched')),
37 ('x', _("The numerical value of 'old'")),
38 ('old.upper()', _("Convert match to uppercase")),
39 ('x * 2', _("Double all matched numbers")),
42 class Search(Minibuffer):
43 "A minibuffer used to search for text."
44 dir = 1
46 def setup(self, window):
47 self.window = window
48 buffer = window.buffer
49 s, e = window.get_selection_range()
50 self.window.mini_entry.set_text(buffer.get_text(s, e, False))
51 cursor = buffer.get_iter_at_mark(window.insert_mark)
52 buffer.move_mark_by_name('search_base', cursor)
53 self.dir = 1
54 self.set_label()
56 fwd = rox.ButtonMixed(g.STOCK_GO_DOWN, _('Find Next'))
57 fwd.set_relief(g.RELIEF_NONE)
58 fwd.unset_flags(g.CAN_FOCUS)
59 fwd.connect('clicked', lambda e: self.set_dir(1))
61 rev = rox.ButtonMixed(g.STOCK_GO_UP, _('Find Previous'))
62 rev.set_relief(g.RELIEF_NONE)
63 rev.unset_flags(g.CAN_FOCUS)
64 rev.connect('clicked', lambda e: self.set_dir(-1))
66 case = g.CheckButton(label=_('Match case'))
67 case.set_relief(g.RELIEF_NONE)
68 case.unset_flags(g.CAN_FOCUS)
70 self.window.mini_hbox.pack_start(fwd, False, True, 0)
71 self.window.mini_hbox.pack_start(rev, False, True, 0)
72 # self.window.mini_hbox.pack_start(case, False, True, 10)
74 self.items = [fwd, rev] #, case]
76 def close(self):
77 for x in self.items:
78 self.window.mini_hbox.remove(x)
80 info = _('Type a string to search for. The display will scroll to show the ' \
81 'next match as you type. Use the Up and Down cursor keys to move ' \
82 'to the next or previous match. Press Escape or Return to finish.')
84 def set_label(self):
85 self.window.set_mini_label(_(' Find: '))
87 def set_dir(self, dir):
88 assert dir == 1 or dir == -1
90 buffer = self.window.buffer
91 cursor = buffer.get_iter_at_mark(self.window.insert_mark)
92 buffer.move_mark_by_name('search_base', cursor)
93 self.dir = dir
95 if dir == 1:
96 cursor.forward_char()
97 else:
98 cursor.backward_char()
99 if self.search(cursor):
100 buffer.move_mark_by_name('search_base', cursor)
101 else:
102 g.gdk.beep()
104 self.changed()
106 def activate(self):
107 self.set_dir(self.dir)
109 def key_press(self, kev):
110 k = kev.keyval
111 if k == g.keysyms.Up:
112 self.set_dir(-1)
113 elif k == g.keysyms.Down:
114 self.set_dir(1)
115 else:
116 return 0
117 return 1
119 def search(self, start):
120 "Search forwards or backwards for the pattern. Matches at 'start'"
121 "are allowed in both directions. Returns (match_start, match_end) if"
122 "found."
123 iter = start.copy()
124 pattern = self.window.mini_entry.get_text()
125 if not pattern:
126 return (iter, iter)
127 if self.dir == 1:
128 found = iter.forward_search(pattern, 0, None)
129 else:
130 iter.forward_chars(len(pattern))
131 found = iter.backward_search(pattern, 0, None)
132 return found
134 def changed(self):
135 buffer = self.window.buffer
136 pos = buffer.get_iter_at_mark(self.window.search_base)
138 found = self.search(pos)
139 if found:
140 buffer.move_mark_by_name('insert', found[0])
141 buffer.move_mark_by_name('selection_bound', found[1])
142 self.window.text.scroll_to_iter(found[0], 0.05, False)
143 else:
144 g.gdk.beep()
146 class RegexHelp(g.ScrolledWindow):
147 def __init__(self):
148 g.ScrolledWindow.__init__(self)
149 self.set_shadow_type(g.SHADOW_IN)
150 self.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
152 model = g.ListStore(str, str)
153 view = g.TreeView(model)
154 self.add(view)
155 view.show()
157 cell = g.CellRendererText()
158 column = g.TreeViewColumn(_('Code'), cell, text = 0)
159 view.append_column(column)
160 column = g.TreeViewColumn(_('Meaning'), cell, text = 1)
161 view.append_column(column)
163 for c, m in regex_help:
164 new = model.append()
165 model.set(new, 0, c, 1, m)
167 self.set_size_request(-1, 150)
169 view.get_selection().set_mode(g.SELECTION_NONE)
171 history = {} # Field name -> last value
173 class Replace(rox.Dialog):
174 def __init__(self, window):
175 self.edit_window = window
176 rox.Dialog.__init__(self, parent = window,
177 flags = g.DIALOG_DESTROY_WITH_PARENT |
178 g.DIALOG_NO_SEPARATOR)
179 self.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
180 self.add_button(g.STOCK_FIND_AND_REPLACE, g.RESPONSE_OK)
181 self.set_default_response(g.RESPONSE_OK)
183 def response(dialog, resp):
184 if resp == g.RESPONSE_OK:
185 self.do_replace()
186 else:
187 self.destroy()
188 self.connect('response', response)
190 vbox = g.VBox(False, 5)
191 self.vbox.pack_start(vbox, True, True, 0)
192 vbox.set_border_width(5)
193 self.sizegroup = g.SizeGroup(g.SIZE_GROUP_HORIZONTAL)
195 def field(name):
196 hbox = g.HBox(False, 2)
197 vbox.pack_start(hbox, False, True, 0)
198 entry = g.Entry()
199 label = g.Label(name)
200 self.sizegroup.add_widget(label)
201 hbox.pack_start(label, False, True, 0)
202 hbox.pack_start(entry, True, True, 0)
203 entry.set_text(history.get(name, ''))
204 def changed(entry):
205 history[name] = entry.get_text()
206 entry.connect('changed', changed)
207 entry.set_activates_default(True)
209 return entry
211 self.replace_entry = field(_('Replace:'))
212 self.with_entry = field(_('With:'))
215 hbox = g.HBox(False)
216 label = g.Label()
217 self.sizegroup.add_widget(label)
218 hbox.pack_start(label, False, False, 3)
219 self.regex = g.CheckButton(_('Advanced search and replace'))
220 hbox.pack_start(self.regex, False, False, 0)
221 vbox.pack_start(hbox, False, True, 0)
222 self.vbox.show_all()
224 regex_help = RegexHelp()
225 vbox.pack_start(regex_help, True, True, 0)
227 self.python_with = g.CheckButton(_("Evaluate 'With' as Python expression"))
228 def changed(toggle): history['Python'] = toggle.get_active()
229 vbox.pack_start(self.python_with, False, True, 0)
230 self.python_with.set_active(history.get('Python', False))
231 self.python_with.connect('toggled', changed)
233 def changed(toggle):
234 history['Advanced'] = toggle.get_active()
235 if toggle.get_active():
236 regex_help.show()
237 self.python_with.show()
238 else:
239 regex_help.hide()
240 self.python_with.hide()
241 self.resize(1, 1)
242 self.regex.connect('toggled', changed)
243 self.regex.set_active(history.get('Advanced', False))
245 def do_replace(self, show_info = True):
246 regex = self.regex.get_active()
248 replace = self.replace_entry.get_text()
249 if not replace:
250 rox.alert(_('You need to specify something to search for...'))
251 return
252 with = self.with_entry.get_text()
254 changes = [0]
255 if regex:
256 import re
257 try:
258 prog = re.compile(replace)
259 except:
260 rox.report_exception()
261 return
262 python = self.python_with.get_active()
263 if python:
264 try:
265 code = compile(with, 'With', 'eval')
266 def with(match):
267 locals = {'old': match.group(0)}
268 try:
269 locals['x'] = float(locals['old'])
270 except:
271 locals['x'] = None
272 return str(eval(code, locals))
273 except:
274 rox.report_exception()
275 return
276 def do_line(line):
277 new, n = prog.subn(with, line)
278 if n:
279 changes[0] += 1
280 return new
281 else:
282 def do_line(line):
283 new = line.replace(replace, with)
284 if new == line:
285 return None
286 changes[0] += 1
287 return new
289 try:
290 self.edit_window.process_selected(do_line)
291 except:
292 rox.report_exception()
293 return
294 if not changes[0]:
295 rox.alert(_('Search string not found'))
296 return
297 if show_info:
298 if changes[0] == 1:
299 rox.info(_('One line changed'))
300 else:
301 rox.info(_('%d lines changed') % changes[0])
302 self.destroy()