dte test coverage


Directory: ./
File: src/search.c
Date: 2025-11-12 12:04:10
Coverage Exec Excl Total
Lines: 88.6% 93 0 105
Functions: 87.5% 7 0 8
Branches: 65.6% 42 0 64

Line Branch Exec Source
1 #include <stdlib.h>
2 #include "search.h"
3 #include "block-iter.h"
4 #include "buffer.h"
5 #include "command/error.h"
6 #include "editor.h"
7 #include "regexp.h"
8 #include "util/ascii.h"
9 #include "util/xmalloc.h"
10 #include "window.h"
11
12 // Recurses at most once
13 // NOLINTNEXTLINE(misc-no-recursion)
14 8 static bool do_search_fwd(View *view, regex_t *regex, BlockIter *bi, bool skip)
15 {
16 8 int flags = block_iter_is_bol(bi) ? 0 : REG_NOTBOL;
17
18 16 do {
19
2/2
✓ Branch 3 → 4 taken 14 times.
✓ Branch 3 → 18 taken 2 times.
16 if (block_iter_is_eof(bi)) {
20 6 return false;
21 }
22
23 14 regmatch_t match;
24 14 StringView line = block_iter_get_line(bi);
25
26 // NOTE: If this is the first iteration then `line.data` contains
27 // a partial line (text starting from the cursor position) and if
28 // `match.rm_so` is 0 then the match is at the beginning of the
29 // text, which is the same as the cursor position.
30
2/2
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 15 taken 10 times.
14 if (regexp_exec(regex, line.data, line.length, 1, &match, flags)) {
31
3/4
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 13 taken 3 times.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 13 not taken.
4 if (skip && match.rm_so == 0) {
32 // Ignore match at current cursor position
33 1 regoff_t count = match.rm_eo;
34
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1 time.
1 if (count == 0) {
35 // It is safe to skip one byte because every line
36 // has one extra byte (newline) that is not in line.data
37 count = 1;
38 }
39 1 block_iter_skip_bytes(bi, (size_t)count);
40 1 return do_search_fwd(view, regex, bi, false);
41 }
42
43 3 block_iter_skip_bytes(bi, match.rm_so);
44 3 view->cursor = *bi;
45 3 view->center_on_scroll = true;
46 3 view_reset_preferred_x(view);
47 3 return true;
48 }
49
50 10 skip = false; // Not at cursor position any more
51 10 flags = 0;
52
2/2
✓ Branch 16 → 3 taken 8 times.
✓ Branch 16 → 19 taken 2 times.
10 } while (block_iter_next_line(bi));
53
54 return false;
55 }
56
57 2 static bool do_search_bwd(View *view, regex_t *regex, BlockIter *bi, ssize_t cx, bool skip)
58 {
59
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
2 if (block_iter_is_eof(bi)) {
60 1 goto next;
61 }
62
63 5 do {
64 5 regmatch_t match;
65 5 int flags = 0;
66 5 regoff_t offset = -1;
67 5 regoff_t pos = 0;
68 5 StringView line = block_iter_get_line(bi);
69
70 5 while (
71 6 pos <= line.length
72
3/4
✓ Branch 11 → 12 taken 6 times.
✗ Branch 11 → 14 not taken.
✓ Branch 13 → 6 taken 2 times.
✓ Branch 13 → 14 taken 4 times.
6 && regexp_exec(regex, line.data + pos, line.length - pos, 1, &match, flags)
73 ) {
74 2 flags = REG_NOTBOL;
75
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 10 taken 1 time.
2 if (cx >= 0) {
76
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 14 taken 1 time.
1 if (pos + match.rm_so >= cx) {
77 // Ignore match at or after cursor
78 break;
79 }
80 if (skip && pos + match.rm_eo > cx) {
81 // Search -rw should not find word under cursor
82 break;
83 }
84 }
85
86 // This might be what we want (last match before cursor)
87 1 offset = pos + match.rm_so;
88 1 pos += match.rm_eo;
89
90
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 14 not taken.
1 if (match.rm_so == match.rm_eo) {
91 // Zero length match
92 break;
93 }
94 }
95
96
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 18 taken 4 times.
5 if (offset >= 0) {
97 1 block_iter_skip_bytes(bi, offset);
98 1 view->cursor = *bi;
99 1 view->center_on_scroll = true;
100 1 view_reset_preferred_x(view);
101 1 return true;
102 }
103
104 4 next:
105 5 cx = -1;
106
2/2
✓ Branch 20 → 4 taken 4 times.
✓ Branch 20 → 21 taken 1 time.
5 } while (block_iter_prev_line(bi));
107
108 return false;
109 }
110
111 1 bool search_tag(View *view, const char *pattern)
112 {
113 // DEFAULT_REGEX_FLAGS is not used here because pattern has been
114 // escaped by parse_ex_pattern() for use as a POSIX BRE
115 1 regex_t regex;
116 1 int err = regcomp(&regex, pattern, REG_NEWLINE);
117
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
1 if (unlikely(err)) {
118 regexp_error_msg(&view->window->editor->err, &regex, pattern, err);
119 }
120
121 1 BlockIter bi = block_iter(view->buffer);
122 1 bool found = do_search_fwd(view, &regex, &bi, false);
123 1 regfree(&regex);
124
125
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
1 if (!found) {
126 // Don't center view to cursor unnecessarily
127 view->force_center = false;
128 return error_msg(&view->window->editor->err, "Tag not found");
129 }
130
131 1 view->center_on_scroll = true;
132 1 return true;
133 }
134
135 static bool has_upper(const char *str)
136 {
137 return strview_contains_char_type(strview(str), ASCII_UPPER);
138 }
139
140 4 static bool update_regex(SearchState *search, ErrorBuffer *ebuf, SearchCaseSensitivity cs)
141 {
142 4 const char *pattern = search->pattern;
143
2/6
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 6 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 4 times.
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
4 bool icase = (cs == CSS_FALSE) || (cs == CSS_AUTO && !has_upper(pattern));
144 int flags = REG_NEWLINE | (icase ? REG_ICASE : 0);
145
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 15 taken 2 times.
4 if (flags == search->re_flags) {
146 return true;
147 }
148
149
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 2 times.
2 if (search->re_flags) {
150 regfree(&search->regex);
151 search->re_flags = 0;
152 }
153
154
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 13 not taken.
2 if (regexp_compile(ebuf, &search->regex, pattern, flags)) {
155 2 search->re_flags = flags;
156 2 return true;
157 }
158
159 regfree(&search->regex);
160 return false;
161 }
162
163 13 void search_free_regexp(SearchState *search)
164 {
165
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 5 taken 11 times.
13 if (search->re_flags) {
166 2 regfree(&search->regex);
167 2 search->re_flags = 0;
168 }
169 13 free(search->pattern);
170 13 }
171
172 2 void search_set_regexp(SearchState *search, const char *pattern)
173 {
174 2 search_free_regexp(search);
175 2 search->pattern = xstrdup(pattern);
176 2 }
177
178 4 bool do_search_next(View *view, SearchState *search, SearchCaseSensitivity cs, bool skip)
179 {
180 4 ErrorBuffer *ebuf = &view->window->editor->err;
181
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 4 times.
4 if (!search->pattern) {
182 return error_msg(ebuf, "No previous search pattern");
183 }
184
1/2
✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 21 not taken.
4 if (!update_regex(search, ebuf, cs)) {
185 return false;
186 }
187
188 4 BlockIter bi = view->cursor;
189 4 regex_t *regex = &search->regex;
190
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 13 taken 1 time.
4 if (!search->reverse) {
191
1/2
✓ Branch 8 → 9 taken 3 times.
✗ Branch 8 → 21 not taken.
3 if (do_search_fwd(view, regex, &bi, true)) {
192 return true;
193 }
194 3 block_iter_bof(&bi);
195
2/2
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 20 taken 1 time.
3 if (do_search_fwd(view, regex, &bi, false)) {
196 2 return info_msg(ebuf, "Continuing at top");
197 }
198 } else {
199 1 size_t cursor_x = block_iter_bol(&bi);
200
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 21 not taken.
1 if (do_search_bwd(view, regex, &bi, cursor_x, skip)) {
201 return true;
202 }
203 1 block_iter_eof(&bi);
204
1/2
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
1 if (do_search_bwd(view, regex, &bi, -1, false)) {
205 1 return info_msg(ebuf, "Continuing at bottom");
206 }
207 }
208
209 1 return error_msg(ebuf, "Pattern '%s' not found", search->pattern);
210 }
211