Added dependency on Python.
[rox-edit.git] / search.py
blobd12d4a4fc6bc3ccfdb10b3118de222eaf951dbce
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
45 last_search = None
47 def setup(self, window):
48 self.window = window
49 buffer = window.buffer
50 s, e = window.get_selection_range()
51 self.window.mini_entry.set_text(buffer.get_text(s, e, False))
52 cursor = buffer.get_iter_at_mark(window.insert_mark)
53 buffer.move_mark_by_name('search_base', cursor)
54 self.dir = 1
55 self.set_label()
57 fwd = rox.ButtonMixed(g.STOCK_GO_DOWN, _('Find Next'))
58 fwd.set_relief(g.RELIEF_NONE)
59 fwd.unset_flags(g.CAN_FOCUS)
60 fwd.connect('clicked', lambda e: self.set_dir(1))
62 rev = rox.ButtonMixed(g.STOCK_GO_UP, _('Find Previous'))
63 rev.set_relief(g.RELIEF_NONE)
64 rev.unset_flags(g.CAN_FOCUS)
65 rev.connect('clicked', lambda e: self.set_dir(-1))
67 case = g.CheckButton(label=_('Match case'))
68 case.set_relief(g.RELIEF_NONE)
69 case.unset_flags(g.CAN_FOCUS)
71 self.window.mini_hbox.pack_start(fwd, False, True, 0)
72 self.window.mini_hbox.pack_start(rev, False, True, 0)
73 # self.window.mini_hbox.pack_start(case, False, True, 10)
75 self.items = [fwd, rev] #, case]
77 def close(self):
78 pattern = self.window.mini_entry.get_text()
79 if pattern:
80 self.last_search = pattern
81 for x in self.items:
82 self.window.mini_hbox.remove(x)
84 info = _('Type a string to search for. The display will scroll to show the ' \
85 'next match as you type. Use the Up and Down cursor keys to move ' \
86 'to the next or previous match. Press Escape or Return to finish.')
88 def set_label(self):
89 self.window.set_mini_label(_(' Find: '))
91 def set_dir(self, dir):
92 assert dir == 1 or dir == -1
94 buffer = self.window.buffer
95 cursor = buffer.get_iter_at_mark(self.window.insert_mark)
96 buffer.move_mark_by_name('search_base', cursor)
97 self.dir = dir
99 if dir == 1:
100 cursor.forward_char()
101 else:
102 cursor.backward_char()
103 if self.search(cursor):
104 buffer.move_mark_by_name('search_base', cursor)
105 else:
106 g.gdk.beep()
108 self.changed()
110 def activate(self):
111 self.window.set_minibuffer(None)
113 def search_again(self):
114 self.set_dir(self.dir)
116 def key_press(self, kev):
117 k = kev.keyval
118 if k == g.keysyms.Up:
119 self.set_dir(-1)
120 elif k == g.keysyms.Down:
121 self.set_dir(1)
122 else:
123 return 0
124 return 1
126 def search(self, start):
127 "Search forwards or backwards for the pattern. Matches at 'start'"
128 "are allowed in both directions. Returns (match_start, match_end) if"
129 "found."
130 iter = start.copy()
131 pattern = self.window.mini_entry.get_text()
132 if not pattern:
133 return (iter, iter)
134 if self.dir == 1:
135 found = iter.forward_search(pattern, 0, None)
136 else:
137 iter.forward_chars(len(pattern))
138 found = iter.backward_search(pattern, 0, None)
139 return found
141 def restore_previous_search(self):
142 if self.last_search:
143 self.window.mini_entry.set_text(self.last_search)
145 def changed(self):
146 buffer = self.window.buffer
147 pos = buffer.get_iter_at_mark(self.window.search_base)
149 found = self.search(pos)
150 if found:
151 buffer.move_mark_by_name('insert', found[0])
152 buffer.move_mark_by_name('selection_bound', found[1])
153 self.window.text.scroll_to_iter(found[0], 0.05, False)
154 else:
155 g.gdk.beep()
157 class RegexHelp(g.ScrolledWindow):
158 def __init__(self):
159 g.ScrolledWindow.__init__(self)
160 self.set_shadow_type(g.SHADOW_IN)
161 self.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
163 model = g.ListStore(str, str)
164 view = g.TreeView(model)
165 self.add(view)
166 view.show()
168 cell = g.CellRendererText()
169 column = g.TreeViewColumn(_('Code'), cell, text = 0)
170 view.append_column(column)
171 column = g.TreeViewColumn(_('Meaning'), cell, text = 1)
172 view.append_column(column)
174 for c, m in regex_help:
175 new = model.append()
176 model.set(new, 0, c, 1, m)
178 self.set_size_request(-1, 150)
180 view.get_selection().set_mode(g.SELECTION_NONE)
182 history = {} # Field name -> last value
184 class Replace(rox.Dialog):
185 def __init__(self, window):
186 self.edit_window = window
187 rox.Dialog.__init__(self, parent = window,
188 flags = g.DIALOG_DESTROY_WITH_PARENT |
189 g.DIALOG_NO_SEPARATOR)
190 self.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
191 self.add_button(g.STOCK_FIND_AND_REPLACE, g.RESPONSE_OK)
192 self.set_default_response(g.RESPONSE_OK)
194 def response(dialog, resp):
195 if resp == int(g.RESPONSE_OK):
196 self.do_replace()
197 else:
198 self.destroy()
199 self.connect('response', response)
201 vbox = g.VBox(False, 5)
202 self.vbox.pack_start(vbox, True, True, 0)
203 vbox.set_border_width(5)
204 self.sizegroup = g.SizeGroup(g.SIZE_GROUP_HORIZONTAL)
206 def field(name):
207 hbox = g.HBox(False, 2)
208 vbox.pack_start(hbox, False, True, 0)
209 entry = g.Entry()
210 label = g.Label(name)
211 self.sizegroup.add_widget(label)
212 hbox.pack_start(label, False, True, 0)
213 hbox.pack_start(entry, True, True, 0)
214 entry.set_text(history.get(name, ''))
215 def changed(entry):
216 history[name] = entry.get_text()
217 entry.connect('changed', changed)
218 entry.set_activates_default(True)
220 return entry
222 self.replace_entry = field(_('Replace:'))
223 self.with_entry = field(_('With:'))
226 hbox = g.HBox(False)
227 label = g.Label()
228 self.sizegroup.add_widget(label)
229 hbox.pack_start(label, False, False, 3)
230 self.regex = g.CheckButton(_('Advanced search and replace'))
231 hbox.pack_start(self.regex, False, False, 0)
232 vbox.pack_start(hbox, False, True, 0)
233 self.vbox.show_all()
235 regex_help = RegexHelp()
236 vbox.pack_start(regex_help, True, True, 0)
238 self.python_with = g.CheckButton(_("Evaluate 'With' as Python expression"))
239 def changed(toggle): history['Python'] = toggle.get_active()
240 vbox.pack_start(self.python_with, False, True, 0)
241 self.python_with.set_active(history.get('Python', False))
242 self.python_with.connect('toggled', changed)
244 def changed(toggle):
245 history['Advanced'] = toggle.get_active()
246 if toggle.get_active():
247 regex_help.show()
248 self.python_with.show()
249 else:
250 regex_help.hide()
251 self.python_with.hide()
252 self.resize(1, 1)
253 self.regex.connect('toggled', changed)
254 self.regex.set_active(history.get('Advanced', False))
256 def do_replace(self, show_info = True):
257 regex = self.regex.get_active()
259 replace = self.replace_entry.get_text()
260 if not replace:
261 rox.alert(_('You need to specify something to search for...'))
262 return
263 replacement = self.with_entry.get_text()
265 changes = [0]
266 if regex:
267 import re
268 try:
269 prog = re.compile(replace)
270 except:
271 rox.report_exception()
272 return
273 python = self.python_with.get_active()
274 if python:
275 try:
276 code = compile(replacement, 'With', 'eval')
277 def replacement(match):
278 locals = {'old': match.group(0)}
279 try:
280 locals['x'] = float(locals['old'])
281 except:
282 locals['x'] = None
283 return str(eval(code, locals))
284 except:
285 rox.report_exception()
286 return
287 def do_line(line):
288 new, n = prog.subn(replacement, line)
289 if n:
290 changes[0] += 1
291 return new
292 else:
293 def do_line(line):
294 new = line.replace(replace, replacement)
295 if new == line:
296 return None
297 changes[0] += 1
298 return new
300 try:
301 self.edit_window.process_selected(do_line)
302 except:
303 rox.report_exception()
304 return
305 if not changes[0]:
306 rox.alert(_('Search string not found'))
307 return
308 if show_info:
309 if changes[0] == 1:
310 rox.info(_('One line changed'))
311 else:
312 rox.info(_('%d lines changed') % changes[0])
313 self.destroy()