1 #include "search.h"
2 #include "buffer.h"
3 #include "change.h"
4 #include "edit.h"
5 #include "editor.h"
6 #include "error.h"
7 #include "regexp.h"
8 #include "selection.h"
9 #include "util/ascii.h"
10 #include "util/string.h"
11 #include "util/xmalloc.h"
12 #include "view.h"
13 
14 #define MAX_SUBSTRINGS 32
15 
do_search_fwd(regex_t * regex,BlockIter * bi,bool skip)16 static bool do_search_fwd(regex_t *regex, BlockIter *bi, bool skip)
17 {
18     int flags = block_iter_is_bol(bi) ? 0 : REG_NOTBOL;
19 
20     do {
21         regmatch_t match;
22         LineRef lr;
23 
24         if (block_iter_is_eof(bi)) {
25             return false;
26         }
27 
28         fill_line_ref(bi, &lr);
29 
30         // NOTE: If this is the first iteration then lr.line contains
31         // partial line (text starting from the cursor position) and
32         // if match.rm_so is 0 then match is at beginning of the text
33         // which is same as the cursor position.
34         if (regexp_exec(regex, lr.line, lr.size, 1, &match, flags)) {
35             if (skip && match.rm_so == 0) {
36                 // Ignore match at current cursor position
37                 regoff_t count = match.rm_eo;
38                 if (count == 0) {
39                     // It is safe to skip one byte because every line
40                     // has one extra byte (newline) that is not in lr.line
41                     count = 1;
42                 }
43                 block_iter_skip_bytes(bi, (size_t)count);
44                 return do_search_fwd(regex, bi, false);
45             }
46 
47             block_iter_skip_bytes(bi, match.rm_so);
48             view->cursor = *bi;
49             view->center_on_scroll = true;
50             view_reset_preferred_x(view);
51             return true;
52         }
53         skip = false; // Not at cursor position anymore
54         flags = 0;
55     } while (block_iter_next_line(bi));
56     return false;
57 }
58 
do_search_bwd(regex_t * regex,BlockIter * bi,ssize_t cx,bool skip)59 static bool do_search_bwd(regex_t *regex, BlockIter *bi, ssize_t cx, bool skip)
60 {
61     if (block_iter_is_eof(bi)) {
62         goto next;
63     }
64 
65     do {
66         regmatch_t match;
67         LineRef lr;
68         int flags = 0;
69         regoff_t offset = -1;
70         regoff_t pos = 0;
71 
72         fill_line_ref(bi, &lr);
73         while (
74             pos <= lr.size
75             && regexp_exec(regex, lr.line + pos, lr.size - pos, 1, &match, flags)
76         ) {
77             flags = REG_NOTBOL;
78             if (cx >= 0) {
79                 if (pos + match.rm_so >= cx) {
80                     // Ignore match at or after cursor
81                     break;
82                 }
83                 if (skip && pos + match.rm_eo > cx) {
84                     // Search -rw should not find word under cursor
85                     break;
86                 }
87             }
88 
89             // This might be what we want (last match before cursor)
90             offset = pos + match.rm_so;
91             pos += match.rm_eo;
92 
93             if (match.rm_so == match.rm_eo) {
94                 // Zero length match
95                 break;
96             }
97         }
98 
99         if (offset >= 0) {
100             block_iter_skip_bytes(bi, offset);
101             view->cursor = *bi;
102             view->center_on_scroll = true;
103             view_reset_preferred_x(view);
104             return true;
105         }
106 next:
107         cx = -1;
108     } while (block_iter_prev_line(bi));
109     return false;
110 }
111 
search_tag(const char * pattern,bool * err)112 bool search_tag(const char *pattern, bool *err)
113 {
114     BlockIter bi = BLOCK_ITER_INIT(&buffer->blocks);
115     regex_t regex;
116     bool found = false;
117 
118     if (!regexp_compile_basic(&regex, pattern, REG_NEWLINE)) {
119         *err = true;
120     } else if (do_search_fwd(&regex, &bi, false)) {
121         view->center_on_scroll = true;
122         found = true;
123     } else {
124         // Don't center view to cursor unnecessarily
125         view->force_center = false;
126         error_msg("Tag not found.");
127         *err = true;
128     }
129     regfree(&regex);
130     return found;
131 }
132 
133 static struct {
134     regex_t regex;
135     char *pattern;
136     SearchDirection direction;
137 
138     // If zero then regex hasn't been compiled
139     int re_flags;
140 } current_search;
141 
search_set_direction(SearchDirection dir)142 void search_set_direction(SearchDirection dir)
143 {
144     current_search.direction = dir;
145 }
146 
current_search_direction(void)147 SearchDirection current_search_direction(void)
148 {
149     return current_search.direction;
150 }
151 
free_regex(void)152 static void free_regex(void)
153 {
154     if (current_search.re_flags) {
155         regfree(&current_search.regex);
156         current_search.re_flags = 0;
157     }
158 }
159 
has_upper(const char * str)160 static bool has_upper(const char *str)
161 {
162     for (size_t i = 0; str[i]; i++) {
163         if (ascii_isupper(str[i])) {
164             return true;
165         }
166     }
167     return false;
168 }
169 
update_regex(void)170 static bool update_regex(void)
171 {
172     int re_flags = REG_NEWLINE;
173 
174     switch (editor.options.case_sensitive_search) {
175     case CSS_TRUE:
176         break;
177     case CSS_FALSE:
178         re_flags |= REG_ICASE;
179         break;
180     case CSS_AUTO:
181         if (!has_upper(current_search.pattern)) {
182             re_flags |= REG_ICASE;
183         }
184         break;
185     }
186 
187     if (re_flags == current_search.re_flags) {
188         return true;
189     }
190 
191     free_regex();
192 
193     current_search.re_flags = re_flags;
194     if (regexp_compile (
195         &current_search.regex,
196         current_search.pattern,
197         current_search.re_flags
198     )) {
199         return true;
200     }
201 
202     free_regex();
203     return false;
204 }
205 
search_set_regexp(const char * pattern)206 void search_set_regexp(const char *pattern)
207 {
208     free_regex();
209     free(current_search.pattern);
210     current_search.pattern = xstrdup(pattern);
211 }
212 
do_search_next(bool skip)213 static void do_search_next(bool skip)
214 {
215     BlockIter bi = view->cursor;
216 
217     if (!current_search.pattern) {
218         error_msg("No previous search pattern.");
219         return;
220     }
221     if (!update_regex()) {
222         return;
223     }
224     if (current_search.direction == SEARCH_FWD) {
225         if (do_search_fwd(&current_search.regex, &bi, true)) {
226             return;
227         }
228 
229         block_iter_bof(&bi);
230         if (do_search_fwd(&current_search.regex, &bi, false)) {
231             info_msg("Continuing at top.");
232         } else {
233             info_msg("Pattern '%s' not found.", current_search.pattern);
234         }
235     } else {
236         size_t cursor_x = block_iter_bol(&bi);
237         if (do_search_bwd(&current_search.regex, &bi, cursor_x, skip)) {
238             return;
239         }
240 
241         block_iter_eof(&bi);
242         if (do_search_bwd(&current_search.regex, &bi, -1, false)) {
243             info_msg("Continuing at bottom.");
244         } else {
245             info_msg("Pattern '%s' not found.", current_search.pattern);
246         }
247     }
248 }
249 
search_prev(void)250 void search_prev(void)
251 {
252     current_search.direction ^= 1;
253     search_next();
254     current_search.direction ^= 1;
255 }
256 
search_next(void)257 void search_next(void)
258 {
259     do_search_next(false);
260 }
261 
search_next_word(void)262 void search_next_word(void)
263 {
264     do_search_next(true);
265 }
266 
build_replacement(String * buf,const char * line,const char * format,regmatch_t * m)267 static void build_replacement (
268     String *buf,
269     const char *line,
270     const char *format,
271     regmatch_t *m
272 ) {
273     size_t i = 0;
274 
275     while (format[i]) {
276         int ch = format[i++];
277 
278         if (ch == '\\') {
279             if (format[i] >= '1' && format[i] <= '9') {
280                 int n = format[i++] - '0';
281                 int len = m[n].rm_eo - m[n].rm_so;
282                 if (len > 0) {
283                     string_add_buf(buf, line + m[n].rm_so, len);
284                 }
285             } else if (format[i] != '\0') {
286                 string_add_byte(buf, format[i++]);
287             }
288         } else if (ch == '&') {
289             int len = m[0].rm_eo - m[0].rm_so;
290             if (len > 0) {
291                 string_add_buf(buf, line + m[0].rm_so, len);
292             }
293         } else {
294             string_add_byte(buf, ch);
295         }
296     }
297 }
298 
299 /*
300  * s/abc/x
301  *
302  * string                to match against
303  * -------------------------------------------
304  * "foo abc bar abc baz" "foo abc bar abc baz"
305  * "foo x bar abc baz"   " bar abc baz"
306  */
replace_on_line(LineRef * lr,regex_t * re,const char * format,BlockIter * bi,ReplaceFlags * flagsp)307 static unsigned int replace_on_line (
308     LineRef *lr,
309     regex_t *re,
310     const char *format,
311     BlockIter *bi,
312     ReplaceFlags *flagsp
313 ) {
314     unsigned char *buf = (unsigned char *)lr->line;
315     ReplaceFlags flags = *flagsp;
316     regmatch_t m[MAX_SUBSTRINGS];
317     size_t pos = 0;
318     int eflags = 0;
319     unsigned int nr = 0;
320 
321     while (regexp_exec (
322         re,
323         buf + pos,
324         lr->size - pos,
325         MAX_SUBSTRINGS,
326         m,
327         eflags
328     )) {
329         regoff_t match_len = m[0].rm_eo - m[0].rm_so;
330         bool skip = false;
331 
332         // Move cursor to beginning of the text to replace
333         block_iter_skip_bytes(bi, m[0].rm_so);
334         view->cursor = *bi;
335 
336         if (flags & REPLACE_CONFIRM) {
337             switch (get_confirmation("Ynaq", "Replace?")) {
338             case 'y':
339                 break;
340             case 'n':
341                 skip = true;
342                 break;
343             case 'a':
344                 flags &= ~REPLACE_CONFIRM;
345                 *flagsp = flags;
346 
347                 // Record rest of the changes as one chain
348                 begin_change_chain();
349                 break;
350             case 'q':
351             case 0:
352                 *flagsp = flags | REPLACE_CANCEL;
353                 goto out;
354             }
355         }
356 
357         if (skip) {
358             // Move cursor after the matched text
359             block_iter_skip_bytes(&view->cursor, match_len);
360         } else {
361             String b = STRING_INIT;
362 
363             build_replacement(&b, buf + pos, format, m);
364 
365             // lineref is invalidated by modification
366             if (buf == lr->line && lr->size != 0) {
367                 buf = xmemdup(buf, lr->size);
368             }
369 
370             buffer_replace_bytes(match_len, b.buffer, b.len);
371             nr++;
372 
373             // Update selection length
374             if (view->selection) {
375                 view->sel_eo += b.len;
376                 view->sel_eo -= match_len;
377             }
378 
379             // Move cursor after the replaced text
380             block_iter_skip_bytes(&view->cursor, b.len);
381             string_free(&b);
382         }
383         *bi = view->cursor;
384 
385         if (!match_len) {
386             break;
387         }
388 
389         if (!(flags & REPLACE_GLOBAL)) {
390             break;
391         }
392 
393         pos += m[0].rm_so + match_len;
394 
395         // Don't match beginning of line again
396         eflags = REG_NOTBOL;
397     }
398 out:
399     if (buf != lr->line) {
400         free(buf);
401     }
402     return nr;
403 }
404 
reg_replace(const char * pattern,const char * format,ReplaceFlags flags)405 void reg_replace(const char *pattern, const char *format, ReplaceFlags flags)
406 {
407     BlockIter bi = BLOCK_ITER_INIT(&buffer->blocks);
408     size_t nr_bytes;
409     bool swapped = false;
410     int re_flags = REG_NEWLINE;
411     unsigned int nr_substitutions = 0;
412     size_t nr_lines = 0;
413     regex_t re;
414 
415     if (pattern[0] == '\0') {
416         error_msg("Search pattern must contain at least 1 character");
417         return;
418     }
419 
420     if (flags & REPLACE_IGNORE_CASE) {
421         re_flags |= REG_ICASE;
422     }
423     if (flags & REPLACE_BASIC) {
424         if (!regexp_compile_basic(&re, pattern, re_flags)) {
425             return;
426         }
427     } else {
428         if (!regexp_compile(&re, pattern, re_flags)) {
429             return;
430         }
431     }
432 
433     if (view->selection) {
434         SelectionInfo info;
435         init_selection(view, &info);
436         view->cursor = info.si;
437         view->sel_so = info.so;
438         view->sel_eo = info.eo;
439         swapped = info.swapped;
440         bi = view->cursor;
441         nr_bytes = info.eo - info.so;
442     } else {
443         BlockIter eof = bi;
444         block_iter_eof(&eof);
445         nr_bytes = block_iter_get_offset(&eof);
446     }
447 
448     // Record multiple changes as one chain only when replacing all
449     if (!(flags & REPLACE_CONFIRM)) {
450         begin_change_chain();
451     }
452 
453     while (1) {
454         // Number of bytes to process
455         size_t count;
456         LineRef lr;
457         unsigned int nr;
458 
459         fill_line_ref(&bi, &lr);
460         count = lr.size;
461         if (lr.size > nr_bytes) {
462             // End of selection is not full line
463             lr.size = nr_bytes;
464         }
465 
466         nr = replace_on_line(&lr, &re, format, &bi, &flags);
467         if (nr) {
468             nr_substitutions += nr;
469             nr_lines++;
470         }
471         if (flags & REPLACE_CANCEL) {
472             break;
473         }
474         if (count + 1 >= nr_bytes) {
475             break;
476         }
477         nr_bytes -= count + 1;
478 
479         block_iter_next_line(&bi);
480     }
481 
482     if (!(flags & REPLACE_CONFIRM)) {
483         end_change_chain();
484     }
485 
486     regfree(&re);
487 
488     if (nr_substitutions) {
489         info_msg("%u substitutions on %zu lines.", nr_substitutions, nr_lines);
490     } else if (!(flags & REPLACE_CANCEL)) {
491         info_msg("Pattern '%s' not found.", pattern);
492     }
493 
494     if (view->selection) {
495         // Undo what init_selection() did
496         if (view->sel_eo) {
497             view->sel_eo--;
498         }
499         if (swapped) {
500             ssize_t tmp = view->sel_so;
501             view->sel_so = view->sel_eo;
502             view->sel_eo = tmp;
503         }
504         block_iter_goto_offset(&view->cursor, view->sel_eo);
505         view->sel_eo = UINT_MAX;
506     }
507 }
508