Parsing the line number option requires Python 2.3. On Python 2.2, just
[rox-edit.git] / search.py
blobbafadf019837929531aedc7083e983c3d40116a9
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)
94 if dir == self.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()
103 else:
104 self.dir = dir
105 self.set_label()
106 self.changed()
108 def activate(self):
109 self.set_dir(self.dir)
111 def key_press(self, kev):
112 k = kev.keyval
113 if k == g.keysyms.Up:
114 self.set_dir(-1)
115 elif k == g.keysyms.Down:
116 self.set_dir(1)
117 else:
118 return 0
119 return 1
121 def search(self, start):
122 "Search forwards or backwards for the pattern. Matches at 'start'"
123 "are allowed in both directions. Returns (match_start, match_end) if"
124 "found."
125 iter = start.copy()
126 pattern = self.window.mini_entry.get_text()
127 if not pattern:
128 return (iter, iter)
129 if self.dir == 1:
130 found = iter.forward_search(pattern, 0, None)
131 else:
132 iter.forward_chars(len(pattern))
133 found = iter.backward_search(pattern, 0, None)
134 return found
136 def changed(self):
137 buffer = self.window.buffer
138 pos = buffer.get_iter_at_mark(self.window.search_base)
140 found = self.search(pos)
141 if found:
142 buffer.move_mark_by_name('insert', found[0])
143 buffer.move_mark_by_name('selection_bound', found[1])
144 self.window.text.scroll_to_iter(found[0], 0.05, False)
145 else:
146 g.gdk.beep()
148 class RegexHelp(g.ScrolledWindow):
149 def __init__(self):
150 g.ScrolledWindow.__init__(self)
151 self.set_shadow_type(g.SHADOW_IN)
152 self.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
154 model = g.ListStore(str, str)
155 view = g.TreeView(model)
156 self.add(view)
157 view.show()
159 cell = g.CellRendererText()
160 column = g.TreeViewColumn(_('Code'), cell, text = 0)
161 view.append_column(column)
162 column = g.TreeViewColumn(_('Meaning'), cell, text = 1)
163 view.append_column(column)
165 for c, m in regex_help:
166 new = model.append()
167 model.set(new, 0, c, 1, m)
169 self.set_size_request(-1, 150)
171 view.get_selection().set_mode(g.SELECTION_NONE)
173 history = {} # Field name -> last value
175 class Replace(rox.Dialog):
176 def __init__(self, window):
177 self.edit_window = window
178 rox.Dialog.__init__(self, parent = window,
179 flags = g.DIALOG_DESTROY_WITH_PARENT |
180 g.DIALOG_NO_SEPARATOR)
181 self.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
182 self.add_button(g.STOCK_FIND_AND_REPLACE, g.RESPONSE_OK)
183 self.set_default_response(g.RESPONSE_OK)
185 def response(dialog, resp):
186 if resp == g.RESPONSE_OK:
187 self.do_replace()
188 else:
189 self.destroy()
190 self.connect('response', response)
192 vbox = g.VBox(False, 5)
193 self.vbox.pack_start(vbox, True, True, 0)
194 vbox.set_border_width(5)
195 self.sizegroup = g.SizeGroup(g.SIZE_GROUP_HORIZONTAL)
197 def field(name):
198 hbox = g.HBox(False, 2)
199 vbox.pack_start(hbox, False, True, 0)
200 entry = g.Entry()
201 label = g.Label(name)
202 self.sizegroup.add_widget(label)
203 hbox.pack_start(label, False, True, 0)
204 hbox.pack_start(entry, True, True, 0)
205 entry.set_text(history.get(name, ''))
206 def changed(entry):
207 history[name] = entry.get_text()
208 entry.connect('changed', changed)
209 entry.set_activates_default(True)
211 return entry
213 self.replace_entry = field(_('Replace:'))
214 self.with_entry = field(_('With:'))
217 hbox = g.HBox(False)
218 label = g.Label()
219 self.sizegroup.add_widget(label)
220 hbox.pack_start(label, False, False, 3)
221 self.regex = g.CheckButton(_('Advanced search and replace'))
222 hbox.pack_start(self.regex, False, False, 0)
223 vbox.pack_start(hbox, False, True, 0)
224 self.vbox.show_all()
226 regex_help = RegexHelp()
227 vbox.pack_start(regex_help, True, True, 0)
229 self.python_with = g.CheckButton(_("Evaluate 'With' as Python expression"))
230 def changed(toggle): history['Python'] = toggle.get_active()
231 vbox.pack_start(self.python_with, False, True, 0)
232 self.python_with.set_active(history.get('Python', False))
233 self.python_with.connect('toggled', changed)
235 def changed(toggle):
236 history['Advanced'] = toggle.get_active()
237 if toggle.get_active():
238 regex_help.show()
239 self.python_with.show()
240 else:
241 regex_help.hide()
242 self.python_with.hide()
243 self.resize(1, 1)
244 self.regex.connect('toggled', changed)
245 self.regex.set_active(history.get('Advanced', False))
247 def do_replace(self, show_info = True):
248 regex = self.regex.get_active()
250 replace = self.replace_entry.get_text()
251 if not replace:
252 rox.alert(_('You need to specify something to search for...'))
253 return
254 with = self.with_entry.get_text()
256 changes = [0]
257 if regex:
258 import re
259 try:
260 prog = re.compile(replace)
261 except:
262 rox.report_exception()
263 return
264 python = self.python_with.get_active()
265 if python:
266 try:
267 code = compile(with, 'With', 'eval')
268 def with(match):
269 locals = {'old': match.group(0)}
270 try:
271 locals['x'] = float(locals['old'])
272 except:
273 locals['x'] = None
274 return str(eval(code, locals))
275 except:
276 rox.report_exception()
277 return
278 def do_line(line):
279 new, n = prog.subn(with, line)
280 if n:
281 changes[0] += 1
282 return new
283 else:
284 def do_line(line):
285 new = line.replace(replace, with)
286 if new == line:
287 return None
288 changes[0] += 1
289 return new
291 try:
292 self.edit_window.process_selected(do_line)
293 except:
294 rox.report_exception()
295 return
296 if not changes[0]:
297 rox.alert(_('Search string not found'))
298 return
299 if show_info:
300 if changes[0] == 1:
301 rox.info(_('One line changed'))
302 else:
303 rox.info(_('%d lines changed') % changes[0])
304 self.destroy()