Support older versions of pygtk (reported by Alastair Porter).
[rox-edit.git] / search.py
blobc2bd11a637071d5b23e36e1c518657614daca4c3
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."
45 def setup(self, window):
46 self.window = window
47 buffer = window.buffer
48 cursor = buffer.get_iter_at_mark(window.insert_mark)
49 buffer.move_mark_by_name('search_base', cursor)
50 self.dir = 1
51 self.set_label()
53 info = 'Type a string to search for. The display will scroll to show the ' \
54 'next match as you type. Use the Up and Down cursor keys to move ' \
55 'to the next or previous match. Press Escape or Return to finish.'
57 def set_label(self):
58 if self.dir == 1:
59 self.window.set_mini_label('Forward search:')
60 else:
61 self.window.set_mini_label('Backward search:')
63 def set_dir(self, dir):
64 assert dir == 1 or dir == -1
66 buffer = self.window.buffer
67 cursor = buffer.get_iter_at_mark(self.window.insert_mark)
68 buffer.move_mark_by_name('search_base', cursor)
70 if dir == self.dir:
71 if dir == 1:
72 cursor.forward_char()
73 else:
74 cursor.backward_char()
75 if self.search(cursor):
76 buffer.move_mark_by_name('search_base', cursor)
77 else:
78 g.gdk.beep()
79 else:
80 self.dir = dir
81 self.set_label()
82 self.changed()
84 def activate(self):
85 self.window.set_minibuffer(None)
87 def key_press(self, kev):
88 k = kev.keyval
89 if k == g.keysyms.Up:
90 self.set_dir(-1)
91 elif k == g.keysyms.Down:
92 self.set_dir(1)
93 else:
94 return 0
95 return 1
97 def search(self, start):
98 "Search forwards or backwards for the pattern. Matches at 'start'"
99 "are allowed in both directions. Returns (match_start, match_end) if"
100 "found."
101 iter = start.copy()
102 pattern = self.window.mini_entry.get_text()
103 if not pattern:
104 return (iter, iter)
105 if self.dir == 1:
106 found = iter.forward_search(pattern, 0, None)
107 else:
108 iter.forward_chars(len(pattern))
109 found = iter.backward_search(pattern, 0, None)
110 return found
112 def changed(self):
113 buffer = self.window.buffer
114 pos = buffer.get_iter_at_mark(self.window.search_base)
116 found = self.search(pos)
117 if found:
118 buffer.move_mark_by_name('insert', found[0])
119 buffer.move_mark_by_name('selection_bound', found[1])
120 self.window.text.scroll_to_iter(found[0], 0.05, g.FALSE)
121 else:
122 g.gdk.beep()
124 class RegexHelp(g.ScrolledWindow):
125 def __init__(self):
126 g.ScrolledWindow.__init__(self)
127 self.set_shadow_type(g.SHADOW_IN)
128 self.set_policy(g.POLICY_NEVER, g.POLICY_AUTOMATIC)
130 model = g.ListStore(str, str)
131 view = g.TreeView(model)
132 self.add(view)
133 view.show()
135 cell = g.CellRendererText()
136 column = g.TreeViewColumn('Code', cell, text = 0)
137 view.append_column(column)
138 column = g.TreeViewColumn('Meaning', cell, text = 1)
139 view.append_column(column)
141 for c, m in regex_help:
142 new = model.append()
143 model.set(new, 0, c, 1, m)
145 self.set_size_request(-1, 150)
147 view.get_selection().set_mode(g.SELECTION_NONE)
149 history = {} # Field name -> last value
151 class Replace(rox.Dialog):
152 def __init__(self, window):
153 self.edit_window = window
154 rox.Dialog.__init__(self, parent = window,
155 flags = g.DIALOG_DESTROY_WITH_PARENT |
156 g.DIALOG_NO_SEPARATOR)
157 self.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
158 self.add_button(g.STOCK_FIND_AND_REPLACE, g.RESPONSE_OK)
159 self.set_default_response(g.RESPONSE_OK)
161 def response(dialog, resp):
162 if resp == g.RESPONSE_OK:
163 self.do_replace()
164 else:
165 self.destroy()
166 self.connect('response', response)
168 vbox = g.VBox(False, 5)
169 self.vbox.pack_start(vbox, True, True, 0)
170 vbox.set_border_width(5)
172 def field(name):
173 hbox = g.HBox(False, 2)
174 vbox.pack_start(hbox, False, True, 0)
175 entry = g.Entry()
176 hbox.pack_start(g.Label(name), False, True, 0)
177 hbox.pack_start(entry, True, True, 0)
178 entry.set_text(history.get(name, ''))
179 def changed(entry):
180 history[name] = entry.get_text()
181 entry.connect('changed', changed)
182 entry.set_activates_default(True)
183 return entry
185 self.replace_entry = field('Replace:')
186 self.with_entry = field('With:')
188 self.regex = g.CheckButton('Advanced search and replace')
189 vbox.pack_start(self.regex, False, True, 0)
190 self.vbox.show_all()
192 regex_help = RegexHelp()
193 vbox.pack_start(regex_help, True, True, 0)
195 self.python_with = g.CheckButton("Evaluate 'With' as python expression")
196 def changed(toggle): history['Python'] = toggle.get_active()
197 vbox.pack_start(self.python_with, False, True, 0)
198 self.python_with.set_active(history.get('Python', False))
199 self.python_with.connect('toggled', changed)
201 def changed(toggle):
202 history['Advanced'] = toggle.get_active()
203 if toggle.get_active():
204 regex_help.show()
205 self.python_with.show()
206 else:
207 regex_help.hide()
208 self.python_with.hide()
209 self.regex.connect('toggled', changed)
210 self.regex.set_active(history.get('Advanced', False))
212 def do_replace(self):
213 regex = self.regex.get_active()
215 replace = self.replace_entry.get_text()
216 if not replace:
217 rox.alert('You need to specify something to search for...')
218 return
219 with = self.with_entry.get_text()
221 changes = [0]
222 if regex:
223 import re
224 try:
225 prog = re.compile(replace)
226 except:
227 rox.report_exception()
228 return
229 python = self.python_with.get_active()
230 if python:
231 try:
232 code = compile(with, 'With', 'eval')
233 def with(match):
234 locals = {'old': match.group(0)}
235 try:
236 locals['x'] = float(locals['old'])
237 except:
238 locals['x'] = None
239 return str(eval(code, locals))
240 except:
241 rox.report_exception()
242 return
243 def do_line(line):
244 new, n = prog.subn(with, line)
245 if n:
246 changes[0] += 1
247 return new
248 else:
249 def do_line(line):
250 new = line.replace(replace, with)
251 if new == line:
252 return None
253 changes[0] += 1
254 return new
256 try:
257 self.edit_window.process_selected(do_line)
258 except:
259 rox.report_exception()
260 return
261 if not changes[0]:
262 rox.alert('Search string not found')
263 return
264 if changes[0] == 1:
265 rox.info('One substitution')
266 else:
267 rox.info('%d substitutions' % changes[0])
268 self.destroy()