Do not use backslash-b to select backspaces.
[mp-5.x.git] / mp_search.mpsl
blob7a0a81be1b9b308773b7b0fc3deb2c1d22a11537
1 /*
3     Minimum Profit 5.x
4     A Programmer's Text Editor
6     Search and replace.
8     Copyright (C) 1991-2011 Angel Ortega <angel@triptico.com>
10     This program is free software; you can redistribute it and/or
11     modify it under the terms of the GNU General Public License
12     as published by the Free Software Foundation; either version 2
13     of the License, or (at your option) any later version.
15     This program is distributed in the hope that it will be useful,
16     but WITHOUT ANY WARRANTY; without even the implied warranty of
17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18     GNU General Public License for more details.
20     You should have received a copy of the GNU General Public License
21     along with this program; if not, write to the Free Software
22     Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
24     http://www.triptico.com
28 /* successful searches will always be shown in this line */
29 mp.config.move_seek_to_line = 5;
31 /* grep recursivity */
32 mp.config.recursive_grep = 0;
34 /** editor actions **/
36 mp.actions['seek']      = sub (d) {
37     local t = mp.form(
38         [
39             {
40                 label:   L("Text to seek:"),
41                 type:    'text',
42                 history: 'search'
43             },
44             {
45                 label:   L("Case sensitive") ~ ':',
46                 type:    'checkbox',
47                 value:   mp.config.case_sensitive_search
48             }
49         ]
50     );
52     if (t != NULL) {
53         mp.config.case_sensitive_search = t[1];
55         mp.long_op(mp.search, d, mp.backslash_codes(t[0])) ||
56             mp.alert(L("Text not found."));
57     }
59     return d;
62 mp.actions['seek_next'] = sub (d) {
63     mp.long_op(mp.search, d, NULL) || mp.alert(L("Text not found."));
65     return d;
68 mp.actions['seek_prev'] = sub (d) {
69     mp.long_op(mp.search_back, d, NULL) || mp.alert(L("Text not found."));
71     return d;
74 mp.actions['replace']   = sub (d) {
75     local r = mp.form(
76         [
77             {
78                 'label'     => L("Replace text:"),
79                 'type'      => 'text',
80                 'history'   => 'search'
81             },
82             {
83                 'label'     => L("Replace with:"),
84                 'type'      => 'text',
85                 'history'   => 'replace'
86             },
87             {
88                 'label'     => L("Case sensitive") ~ ':',
89                 'type'      => 'checkbox',
90                 'value'     => mp.config.case_sensitive_search
91             },
92             {
93                 'label'     => L("Global replace:"),
94                 'type'      => 'checkbox',
95                 'value'     => mp.config.global_replace
96             }
97         ]
98     );
100     if (r != NULL) {
101         mp.config.case_sensitive_search = r[2];
102         mp.config.global_replace = r[3];
104         mp.store_undo(d);
105         mp.long_op(mp.replace, d, mp.backslash_codes(r[0]), mp.backslash_codes(r[1]));
106     }
108     return d;
111 mp.actions['seek_next_char'] = sub (d) { mp.seek_prev_or_next_char(d, mp.search); };
112 mp.actions['seek_prev_char'] = sub (d) { mp.seek_prev_or_next_char(d, mp.search_back); };
114 mp.actions['grep']      = sub (d) {
115     local r = mp.form(
116         [
117             {
118                 'label'     => L("Text to seek:"),
119                 'type'      => 'text',
120                 'history'   => 'search'
121             },
122             {
123                 'label'     => L("Files to grep (empty, all):"),
124                 'type'      => 'text',
125                 'history'   => 'grep'
126             },
127             {
128                 'label'     => L("Base directory (empty, current):"),
129                 'type'      => 'text',
130                 'history'   => 'grep_base'
131             },
132             {
133                 'label'     => L("Recursive?"),
134                 'type'      => 'checkbox',
135                 'value'     => mp.config.recursive_grep
136             }
137         ]
138     );
140     if (r != NULL && r[0] ne '') {
141         mp.config.recursive_grep = r[3];
143         local t = '<grep ' ~ r[0] ~ ' ' ~ r[1] ~ '>';
145         if ((r = mp.long_op(mp.grep, '/' ~ r[0] ~ '/', r[1], r[2], r[3])) == NULL)
146             mp.alert(L("File(s) not found."));
147         else
148         if (size(r) == 0)
149             mp.alert(L("Text not found."));
150         else {
151             local l = mp.open(t);
153             l.txt.lines = [];
154             mp.move_bof(l);
156             foreach (e, r)
157                 mp.insert(l, sprintf("%s:%d: %s\n", e[0], e[1] + 1, e[2]));
159             mp.move_bof(l);
161             l.txt.mod = 0;
162             l.read_only = 1;
163         }
164     }
166     return d;
169 /** default key bindings **/
171 mp.keycodes['f3']               = 'seek_next';
172 mp.keycodes['ctrl-f3']          = 'seek_prev';
173 mp.keycodes['ctrl-f']           = 'seek';
174 mp.keycodes['ctrl-r']           = 'replace';
175 mp.keycodes['ctrl-page-down']   = 'seek_next_char';
176 mp.keycodes['ctrl-page-up']     = 'seek_prev_char';
178 /** action descriptions **/
180 mp.actdesc['seek']              = LL("Search text...");
181 mp.actdesc['seek_next']         = LL("Search next");
182 mp.actdesc['seek_prev']         = LL("Search previous");
183 mp.actdesc['replace']           = LL("Replace...");
184 mp.actdesc['seek_next_char']    = LL("Move to next instance of current char");
185 mp.actdesc['seek_prev_char']    = LL("Move to previous instance of current char");
186 mp.actdesc['grep']              = LL("Grep (find inside) files...");
188 /** code **/
191  * mp.search_set_y - Sets the y position after a successful search.
193  * Sets the y position after a successful search, setting the
194  * visual line to that defined in mp.config.move_seek_to_line.
195  */
196 sub mp.search_set_y(doc, y)
198         mp.set_y(doc, y);
200         /* set always to the same line */
201         if (mp.config.move_seek_to_line != NULL &&
202            (doc.txt.vy = doc.txt.y - mp.config.move_seek_to_line) < 0)
203                 doc.txt.vy = 0;
205         return doc;
209 sub mp.prefix_regex(str)
210 /* set str to be a valid regex */
212         if (!str)
213                 return NULL;
215         /* surround with / for the regex */
216         str = '/' ~ str ~ '/';
218         /* add optional case insensitivity flag */
219         if (! mp.config.case_sensitive_search)
220                 str = str ~ 'i';
222         return str;
226 sub mp.search_dir(doc, str, dir)
227 /* search str and put the current position there, with direction */
229         local txt, r, l, lines;
230         local bx, by, ex, ey;
232         if (str == NULL)
233                 str = mp.last_search;
234         else {
235                 str = mp.prefix_regex(str);
236                 mp.last_search = str;
237         }
239         if (str == NULL)
240                 return NULL;
242         txt = doc.txt;
244         if (dir == -1) {
245                 /* search backwards */
246                 str = str ~ 'l';
248                 ex = txt.x && txt.x - 1 || 0;
249                 ey = txt.y;
251                 if (txt.mark && !txt.mark.incomplete) {
252                         if (ey >= txt.mark.ey) {
253                                 ey = txt.mark.ey;
255                                 if (ex > txt.mark.ex)
256                                         ex = txt.mark.ex;
257                         }
259                         bx = txt.mark.bx;
260                         by = txt.mark.by;
261                 }
262                 else {
263                         bx = 0;
264                         by = 0;
265                 }
266         }
267         else {
268                 /* search forward */
269                 bx = txt.x;
270                 by = txt.y;
272                 if (txt.mark && !txt.mark.incomplete) {
273                         if (by <= txt.mark.by) {
274                                 by = txt.mark.by;
276                                 if (bx < txt.mark.bx)
277                                         bx = txt.mark.bx;
278                         }
280                         ex = txt.mark.ex;
281                         ey = txt.mark.ey;
282                 }
283                 else {
284                         ex = size(txt.lines[-1]);
285                         ey = size(txt.lines);
286                 }
287         }
289         lines = mp.get_range(doc, bx, by, ex, ey, 0);
291         /* do the search */
292         local n = (dir == -1) && (size(lines) - 1) || 0;
293         while ((l = lines[n]) != NULL && regex(l, str) == NULL)
294                 n += dir;
296         r = regex();
298         if (r) {
299                 /* if it was found in the first line, add offset */
300                 r[0] += (n == 0 && bx);
302                 mp.search_set_y(doc, by + n);
303                 mp.set_x(doc, r[0] + r[1]);
304         }
306         return r;
310 sub mp.search(doc, str)
311 /* search str and put the current position there, downwards */
313         mp.search_dir(doc, str, 1);
317 sub mp.search_back(doc, str)
318 /* search str and put the current position there, backwards */
320         mp.search_dir(doc, str, -1);
324 sub mp.replace_1(doc, org, dst)
325 /* searches 'org' and replaces it with 'dst', once */
327         local c;
329         if ((c = mp.search(doc, org)) != NULL) {
330                 local txt, l;
332                 txt = doc.txt;
334                 /* substitute */
335                 txt.lines[txt.y] = sregex(txt.lines[txt.y],
336                                 mp.prefix_regex(org),
337                                 dst, c[0]
338                             );
340                 /* move to correct position */
341                 c = regex();
342                 mp.set_x(doc, c[0] + c[1]);
344                 txt.mod++;
345         }
347         return c;
351 sub mp.replace(doc, org, dst)
352 /* replaces 'org' with 'that', may be globally */
354         local cnt = 0;
356         while (mp.replace_1(doc, org, dst)) {
357                 cnt++;
359                 if (!mp.config.global_replace)
360                         break;
361         }
363         mp.message = {
364                 'timeout' => time() + 4,
365                 'string'  => sprintf(L("%d replaces"), cnt)
366         };
368         return doc;
372 sub mp.seek_prev_or_next_char(doc, func)
373 /* moves to next or previous occurence of current char */
375         local txt = doc.txt;
377         /* get current char */
378         local w = splice(txt.lines[txt.y], NULL, txt.x, 1);
380         /* move one char right */
381         mp.move_right(doc);
383         /* search for it (mp.search() or mp.search_back()) */
384         local t = mp.last_search;
385         func(doc, '\' ~ w[1]);
386         mp.last_search = t;
388         /* move back */
389         mp.move_left(doc);
391         return doc;
395 sub mp.grep(rx, spec, base, rec, r)
396 /* Greps str in the files in spec. Returns NULL if no file matched the glob()
397    (or glob() is unsupported), an empty list if the string was not found or
398    an array with the matches, that are three-element arrays with the file name,
399    the line number and the line that matched */
401         local all;
403         /* if spec is empty, set as NULL (meaning "glob everything") */
404         if (spec eq '')
405                 spec = NULL;
407         all = glob(spec, base);
409         if (r == NULL)
410                 r = [];
412         /* spec globs to NULL or empty; abort */
413         if (size(all) == 0)
414                 return r;
416         foreach (fn, all) {
417                 local f;
419                 if ((f = open(fn, "r")) != NULL) {
420                         local l, n;
422                         /* file open; now grep */
423                         while (l = read(f)) {
424                                 l = mp.chomp(l);
426                                 if (regex(l, rx)) {
427                                         /* found; store line, filename and linenum */
428                                         push(r, [ fn, n, l ]);
429                                 }
431                                 n++;
432                         }
434                         close(f);
435                 }
436         }
438         if (rec) {
439                 /* glob again, trying subdirectories */
440                 foreach (fn, glob(NULL, base)) {
441                         if (regex(fn, '@/$@')) {
442                                 r = mp.grep(rx, spec, fn, rec, r);
443                         }
444                 }
445         }
447         return r;