Line | Count | Source |
1 | | /* $OpenBSD: fuzzy.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */ |
2 | | |
3 | | /* |
4 | | * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> |
5 | | * |
6 | | * Permission to use, copy, modify, and distribute this software for any |
7 | | * purpose with or without fee is hereby granted, provided that the above |
8 | | * copyright notice and this permission notice appear in all copies. |
9 | | * |
10 | | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
11 | | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
12 | | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
13 | | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
14 | | * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER |
15 | | * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING |
16 | | * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
17 | | */ |
18 | | |
19 | | #include <sys/types.h> |
20 | | |
21 | | #include <ctype.h> |
22 | | #include <stdlib.h> |
23 | | #include <string.h> |
24 | | |
25 | | #include "tmux.h" |
26 | | |
27 | | /* |
28 | | * Fuzzy matching in the style of fzf. The pattern is split into groups by | |
29 | | * and each group is split on spaces into terms. A row matches if any group |
30 | | * matches; within a group all positive terms must match and all inverse terms |
31 | | * must not match. |
32 | | * |
33 | | * Plain positive terms are fuzzy subsequences. A leading ' makes a term an |
34 | | * exact substring match, ^ anchors a term at the start and $ anchors it at |
35 | | * the end. A leading ! inverts the term. Plain inverse terms are exact |
36 | | * substring matches rather than inverse fuzzy matches, like fzf. |
37 | | * |
38 | | * Both the pattern and the text are UTF-8. The text may contain tmux style |
39 | | * directives (#[...]); these and their contents are invisible to matching and |
40 | | * occupy no columns, but align= styles do move the surrounding text and are |
41 | | * accounted for exactly as format_draw lays it out (the no-list layout, see |
42 | | * format_draw_none). Matching is smart-case: case is ignored unless the pattern |
43 | | * contains an uppercase character (ASCII case folding only; other characters |
44 | | * are compared exactly by their UTF-8 data). |
45 | | * |
46 | | * On a match a bitstr_t of the requested display width is returned with a bit |
47 | | * set for every column occupied by a matched character, so the caller can |
48 | | * highlight them; NULL is returned if there is no match. A cheap fzf-style |
49 | | * score (matches at the start, after word boundaries and in contiguous runs |
50 | | * score higher) is also produced so callers can rank best-match-first. |
51 | | */ |
52 | | |
53 | 512 | #define FUZZY_BONUS_EXACT 1000 |
54 | 0 | #define FUZZY_BONUS_PREFIX 200 |
55 | 0 | #define FUZZY_BONUS_SUFFIX 100 |
56 | 2 | #define FUZZY_BONUS_START 12 |
57 | 128 | #define FUZZY_BONUS_BOUNDARY 8 |
58 | 514 | #define FUZZY_BONUS_CONSECUTIVE 6 |
59 | 515 | #define FUZZY_PENALTY_LEADING 1 |
60 | 646 | #define FUZZY_PENALTY_LEADING_MAX 10 |
61 | 5 | #define FUZZY_PENALTY_GAP 1 |
62 | | |
63 | | /* A single visible character of the text. */ |
64 | | struct fuzzy_char { |
65 | | enum style_align align; |
66 | | struct utf8_data ud; /* original UTF-8 data */ |
67 | | u_int width; /* display width */ |
68 | | u_int offset; /* within its alignment */ |
69 | | }; |
70 | | |
71 | | /* One parsed query term. */ |
72 | | struct fuzzy_term { |
73 | | int inverse; |
74 | | int exact; |
75 | | int prefix; |
76 | | int suffix; |
77 | | const char *text; |
78 | | size_t len; |
79 | | }; |
80 | | |
81 | | /* Is this character a word boundary, so a match after it scores higher? */ |
82 | | static int |
83 | | fuzzy_is_boundary(const struct utf8_data *ud) |
84 | 527 | { |
85 | 527 | static const char *boundary = " -_/.:"; |
86 | | |
87 | 527 | if (ud->size != 1) |
88 | 0 | return (0); |
89 | 527 | return (strchr(boundary, ud->data[0]) != NULL); |
90 | 527 | } |
91 | | |
92 | | /* |
93 | | * Compare two characters, folding ASCII case if wanted. UTF-8 is compared |
94 | | * directly without case folding. |
95 | | */ |
96 | | static int |
97 | | fuzzy_char_equal(const struct utf8_data *a, const struct utf8_data *b, int fold) |
98 | 119k | { |
99 | 119k | if (fold && |
100 | 60.2k | a->size == 1 && |
101 | 60.2k | b->size == 1 && |
102 | 60.2k | a->data[0] < 0x80 && |
103 | 60.2k | b->data[0] < 0x80) |
104 | 60.2k | return (tolower(a->data[0]) == tolower(b->data[0])); |
105 | 58.9k | return (a->size == b->size && memcmp(a->data, b->data, a->size) == 0); |
106 | 119k | } |
107 | | |
108 | | /* Map a style alignment onto one of the four layout columns. */ |
109 | | static enum style_align |
110 | | fuzzy_align(enum style_align align) |
111 | 354 | { |
112 | 354 | if (align == STYLE_ALIGN_DEFAULT) |
113 | 354 | return (STYLE_ALIGN_LEFT); |
114 | 0 | return (align); |
115 | 354 | } |
116 | | |
117 | | /* Add a visible character to the array, updating the alignment width. */ |
118 | | static void |
119 | | fuzzy_add(struct fuzzy_char **cs, u_int *ncs, u_int *alloc, enum style_align a, |
120 | | const struct utf8_data *ud, u_int *widths) |
121 | 92.5k | { |
122 | 92.5k | struct fuzzy_char *fc; |
123 | | |
124 | 92.5k | if (*ncs == *alloc) { |
125 | 1.76k | *alloc = (*alloc == 0) ? 64 : *alloc * 2; |
126 | 1.76k | *cs = xreallocarray(*cs, *alloc, sizeof **cs); |
127 | 1.76k | } |
128 | 92.5k | fc = &(*cs)[(*ncs)++]; |
129 | 92.5k | fc->align = a; |
130 | 92.5k | memcpy(&fc->ud, ud, sizeof fc->ud); |
131 | 92.5k | fc->width = ud->width; |
132 | 92.5k | fc->offset = widths[a]; |
133 | 92.5k | widths[a] += ud->width; |
134 | 92.5k | } |
135 | | |
136 | | /* Decode a character as UTF-8. */ |
137 | | static const char * |
138 | | fuzzy_decode_one(const char *cp, const char *end, struct utf8_data *ud) |
139 | 121k | { |
140 | 121k | enum utf8_state more; |
141 | 121k | const char *start = cp; |
142 | | |
143 | 121k | if ((more = utf8_open(ud, (u_char)*cp)) == UTF8_MORE) { |
144 | 34.6k | while (++cp != end && more == UTF8_MORE) |
145 | 20.7k | more = utf8_append(ud, (u_char)*cp); |
146 | 13.8k | if (more == UTF8_DONE) |
147 | 0 | return (cp); |
148 | 13.8k | cp = start; |
149 | 13.8k | } |
150 | 121k | utf8_set(ud, (u_char)*cp); |
151 | 121k | return (cp + 1); |
152 | 121k | } |
153 | | |
154 | | /* |
155 | | * Scan the text into an array of visible characters, skipping styles and |
156 | | * recording the alignment and intra-alignment offset of each. Returns the |
157 | | * array and its length and fills in the per-alignment widths. |
158 | | */ |
159 | | static struct fuzzy_char * |
160 | | fuzzy_scan(const char *text, u_int *ncs, u_int *widths) |
161 | 908 | { |
162 | 908 | struct fuzzy_char *cs = NULL; |
163 | 908 | u_int alloc = 0, n, leading, i; |
164 | 908 | enum style_align current = STYLE_ALIGN_LEFT; |
165 | 908 | struct style sy; |
166 | 908 | const char *cp = text, *textend = text + strlen(text); |
167 | 908 | const char *end; |
168 | 908 | struct utf8_data ud, hash, bracket; |
169 | 908 | char *tmp; |
170 | | |
171 | 908 | *ncs = 0; |
172 | 908 | memset(widths, 0, sizeof *widths * (STYLE_ALIGN_ABSOLUTE_CENTRE + 1)); |
173 | 908 | style_set(&sy, &grid_default_cell); |
174 | 908 | utf8_set(&hash, '#'); |
175 | 908 | utf8_set(&bracket, '['); |
176 | | |
177 | 116k | while (*cp != '\0') { |
178 | | /* Handle a run of #s, which may introduce a style. */ |
179 | 115k | if (*cp == '#') { |
180 | 11.2k | for (n = 0; cp[n] == '#'; n++) |
181 | 7.14k | /* nothing */; |
182 | 4.11k | if (cp[n] != '[') { |
183 | | /* Escaped #s: ##->#, so half (rounded up). */ |
184 | 2.22k | leading = (n % 2 == 0) ? n / 2 : n / 2 + 1; |
185 | 4.44k | for (i = 0; i < leading; i++) { |
186 | 2.22k | fuzzy_add(&cs, ncs, &alloc, current, |
187 | 2.22k | &hash, widths); |
188 | 2.22k | } |
189 | 2.22k | cp += n; |
190 | 2.22k | continue; |
191 | 2.22k | } |
192 | | |
193 | | /* Even count: all #s escaped, the [ is literal. */ |
194 | 3.38k | for (i = 0; i < n / 2; i++) |
195 | 1.50k | fuzzy_add(&cs, ncs, &alloc, current, &hash, |
196 | 1.50k | widths); |
197 | 1.88k | if (n % 2 == 0) { |
198 | 227 | fuzzy_add(&cs, ncs, &alloc, current, &bracket, |
199 | 227 | widths); |
200 | 227 | cp += n + 1; |
201 | 227 | continue; |
202 | 227 | } |
203 | | |
204 | | /* Odd count: this is a style, find and parse it. */ |
205 | 1.66k | end = format_skip(cp + n + 1, "]"); |
206 | 1.66k | if (end == NULL) |
207 | 128 | break; |
208 | 1.53k | tmp = xstrndup(cp + n + 1, end - (cp + n + 1)); |
209 | 1.53k | if (style_parse(&sy, &grid_default_cell, tmp) == 0) |
210 | 354 | current = fuzzy_align(sy.align); |
211 | 1.53k | free(tmp); |
212 | 1.53k | cp = end + 1; |
213 | 1.53k | continue; |
214 | 1.66k | } |
215 | | |
216 | | /* Decode one character, multibyte or single byte. */ |
217 | 111k | cp = fuzzy_decode_one(cp, textend, &ud); |
218 | | |
219 | | /* |
220 | | * Skip non-printable single bytes (control characters and raw |
221 | | * bytes left over from a failed decode); keep printable ASCII |
222 | | * and any decoded UTF-8. |
223 | | */ |
224 | 111k | if (ud.size == 1 && (ud.data[0] <= 0x1f || ud.data[0] >= 0x7f)) |
225 | 22.9k | continue; |
226 | 88.5k | fuzzy_add(&cs, ncs, &alloc, current, &ud, widths); |
227 | 88.5k | } |
228 | 908 | return (cs); |
229 | 908 | } |
230 | | |
231 | | /* |
232 | | * Work out the display column of a visible character given the trimmed widths |
233 | | * and start columns of each alignment. Returns 0 and sets the column if the |
234 | | * character is visible, otherwise returns -1. |
235 | | */ |
236 | | static int |
237 | | fuzzy_column(const struct fuzzy_char *fc, const u_int *start, const u_int *src, |
238 | | const u_int *vis, u_int *column) |
239 | 15 | { |
240 | 15 | enum style_align a = fc->align; |
241 | | |
242 | 15 | if (fc->offset < src[a] || fc->offset >= src[a] + vis[a]) |
243 | 0 | return (-1); |
244 | 15 | *column = start[a] + (fc->offset - src[a]); |
245 | 15 | return (0); |
246 | 15 | } |
247 | | |
248 | | /* Decode a UTF-8 term into an array of characters. */ |
249 | | static u_int |
250 | | fuzzy_decode(const char *tok, size_t len, struct utf8_data *out) |
251 | 1.03k | { |
252 | 1.03k | const char *cp = tok, *end = tok + len; |
253 | 1.03k | u_int n = 0; |
254 | | |
255 | 10.5k | while (cp != end) |
256 | 9.52k | cp = fuzzy_decode_one(cp, end, &out[n++]); |
257 | 1.03k | return (n); |
258 | 1.03k | } |
259 | | |
260 | | /* Add the score for a fuzzy token matched at the given positions. */ |
261 | | static int |
262 | | fuzzy_score_positions(const u_int *pos, u_int npos, const struct fuzzy_char *cs) |
263 | 5 | { |
264 | 5 | u_int i, gap, span; |
265 | 5 | int score = 0; |
266 | | |
267 | 5 | if (npos == 0) |
268 | 0 | return (0); |
269 | 5 | if (pos[0] == 0) |
270 | 2 | score += FUZZY_BONUS_START; |
271 | 3 | else { |
272 | 3 | if (fuzzy_is_boundary(&cs[pos[0] - 1].ud)) |
273 | 0 | score += FUZZY_BONUS_BOUNDARY; |
274 | 3 | if (pos[0] < FUZZY_PENALTY_LEADING_MAX) |
275 | 0 | score -= pos[0] * FUZZY_PENALTY_LEADING; |
276 | 3 | else { |
277 | 3 | score -= FUZZY_PENALTY_LEADING_MAX * |
278 | 3 | FUZZY_PENALTY_LEADING; |
279 | 3 | } |
280 | 3 | } |
281 | 19 | for (i = 1; i < npos; i++) { |
282 | 14 | if (pos[i] == pos[i - 1] + 1) |
283 | 2 | score += FUZZY_BONUS_CONSECUTIVE; |
284 | 12 | else if (fuzzy_is_boundary(&cs[pos[i] - 1].ud)) |
285 | 0 | score += FUZZY_BONUS_BOUNDARY; |
286 | 14 | } |
287 | 5 | span = pos[npos - 1] - pos[0] + 1; |
288 | 5 | gap = span - npos; |
289 | 5 | score -= gap * FUZZY_PENALTY_GAP; |
290 | 5 | return (score); |
291 | 5 | } |
292 | | |
293 | | /* |
294 | | * Match a token as a subsequence of the visible characters. Returns if the |
295 | | * token matches. |
296 | | */ |
297 | | static int |
298 | | fuzzy_match_fuzzy(const struct utf8_data *tok, u_int toklen, |
299 | | struct fuzzy_char *cs, u_int ncs, int fold, int *score, char *matched) |
300 | 762 | { |
301 | 762 | u_int pi, ci, *pos; |
302 | 762 | int found, value; |
303 | | |
304 | 762 | if (toklen == 0 || ncs == 0) |
305 | 0 | return (0); |
306 | 762 | pos = xcalloc(toklen, sizeof *pos); |
307 | | |
308 | | /* First find a subsequence from the start. */ |
309 | 762 | ci = 0; |
310 | 1.35k | for (pi = 0; pi < toklen; pi++) { |
311 | 109k | while (ci != ncs && |
312 | 108k | !fuzzy_char_equal(&tok[pi], &cs[ci].ud, fold)) |
313 | 107k | ci++; |
314 | 1.35k | if (ci == ncs) { |
315 | 757 | free(pos); |
316 | 757 | return (0); |
317 | 757 | } |
318 | 595 | pos[pi] = ci++; |
319 | 595 | } |
320 | | |
321 | | /* Then compact it backwards to prefer a shorter span. */ |
322 | 5 | ci = pos[toklen - 1]; |
323 | 24 | for (pi = toklen; pi > 0; pi--) { |
324 | 19 | found = 0; |
325 | 343 | for (;;) { |
326 | 343 | if (fuzzy_char_equal(&tok[pi - 1], &cs[ci].ud, fold)) { |
327 | 19 | pos[pi - 1] = ci; |
328 | 19 | found = 1; |
329 | 19 | break; |
330 | 19 | } |
331 | 324 | if (ci == 0) |
332 | 0 | break; |
333 | 324 | ci--; |
334 | 324 | } |
335 | 19 | if (!found) { |
336 | 0 | free(pos); |
337 | 0 | return (0); |
338 | 0 | } |
339 | 19 | if (pi != 1) |
340 | 14 | ci--; |
341 | 19 | } |
342 | | |
343 | 5 | value = fuzzy_score_positions(pos, toklen, cs); |
344 | 5 | *score += value; |
345 | 24 | for (pi = 0; pi < toklen; pi++) |
346 | 19 | matched[pos[pi]] = 1; |
347 | 5 | free(pos); |
348 | 5 | return (1); |
349 | 5 | } |
350 | | |
351 | | /* Score an exact, prefix or suffix match. */ |
352 | | static int |
353 | | fuzzy_score_exact(u_int start, u_int toklen, u_int ncs, |
354 | | const struct fuzzy_char *cs, int prefix, int suffix) |
355 | 512 | { |
356 | 512 | int score; |
357 | | |
358 | 512 | score = FUZZY_BONUS_EXACT + toklen * FUZZY_BONUS_CONSECUTIVE; |
359 | 512 | if (prefix) |
360 | 0 | score += FUZZY_BONUS_PREFIX; |
361 | 512 | if (suffix) |
362 | 0 | score += FUZZY_BONUS_SUFFIX; |
363 | 512 | if (start == 0) |
364 | 0 | score += FUZZY_BONUS_START; |
365 | 512 | else if (fuzzy_is_boundary(&cs[start - 1].ud)) |
366 | 128 | score += FUZZY_BONUS_BOUNDARY; |
367 | 512 | if (start < FUZZY_PENALTY_LEADING_MAX) |
368 | 384 | score -= start * FUZZY_PENALTY_LEADING; |
369 | 128 | else |
370 | 128 | score -= FUZZY_PENALTY_LEADING_MAX * FUZZY_PENALTY_LEADING; |
371 | 512 | if (!prefix && !suffix) |
372 | 512 | score -= ncs - (start + toklen); |
373 | 512 | return (score); |
374 | 512 | } |
375 | | |
376 | | /* Match an exact, prefix or suffix term against the visible characters. */ |
377 | | static int |
378 | | fuzzy_match_exact(const struct utf8_data *tok, u_int toklen, |
379 | | struct fuzzy_char *cs, u_int ncs, int fold, int prefix, int suffix, |
380 | | int *score, char *matched) |
381 | 268 | { |
382 | 268 | u_int start, end, i, j, best = 0; |
383 | 268 | int ok, found = 0, value, bestscore = 0; |
384 | | |
385 | 268 | if (toklen == 0 || toklen > ncs) |
386 | 0 | return (0); |
387 | | |
388 | 268 | if (prefix && suffix) { |
389 | 0 | if (toklen != ncs) |
390 | 0 | return (0); |
391 | 0 | start = 0; |
392 | 0 | end = 1; |
393 | 268 | } else if (prefix) { |
394 | 128 | start = 0; |
395 | 128 | end = 1; |
396 | 140 | } else if (suffix) { |
397 | 0 | start = ncs - toklen; |
398 | 0 | end = start + 1; |
399 | 140 | } else { |
400 | 140 | start = 0; |
401 | 140 | end = ncs - toklen + 1; |
402 | 140 | } |
403 | | |
404 | 10.5k | for (i = start; i < end; i++) { |
405 | 10.3k | ok = 1; |
406 | 10.8k | for (j = 0; j < toklen; j++) { |
407 | 10.3k | if (!fuzzy_char_equal(&tok[j], &cs[i + j].ud, fold)) { |
408 | 9.80k | ok = 0; |
409 | 9.80k | break; |
410 | 9.80k | } |
411 | 10.3k | } |
412 | 10.3k | if (!ok) |
413 | 9.80k | continue; |
414 | 512 | value = fuzzy_score_exact(i, toklen, ncs, cs, prefix, suffix); |
415 | 512 | if (!found || value > bestscore) { |
416 | 384 | found = 1; |
417 | 384 | best = i; |
418 | 384 | bestscore = value; |
419 | 384 | } |
420 | 512 | } |
421 | 268 | if (!found) |
422 | 140 | return (0); |
423 | 128 | *score += bestscore; |
424 | 128 | if (matched != NULL) { |
425 | 0 | for (i = 0; i < toklen; i++) |
426 | 0 | matched[best + i] = 1; |
427 | 0 | } |
428 | 128 | return (1); |
429 | 268 | } |
430 | | |
431 | | /* Parse one term. */ |
432 | | static int |
433 | | fuzzy_parse_term(const char *start, const char *end, struct fuzzy_term *term) |
434 | 1.03k | { |
435 | 1.03k | memset(term, 0, sizeof *term); |
436 | 1.03k | if (start == end) |
437 | 0 | return (0); |
438 | 1.03k | if (*start == '!') { |
439 | 140 | term->inverse = 1; |
440 | 140 | start++; |
441 | 140 | } |
442 | 1.03k | if (start == end) |
443 | 0 | return (0); |
444 | 1.03k | if (*start == '\'') { |
445 | 8 | term->exact = 1; |
446 | 8 | start++; |
447 | 1.03k | } else if (*start == '^') { |
448 | 128 | term->exact = 1; |
449 | 128 | term->prefix = 1; |
450 | 128 | start++; |
451 | 128 | } |
452 | 1.03k | if (start == end) |
453 | 8 | return (0); |
454 | 1.03k | if (end[-1] == '$') { |
455 | 0 | term->exact = 1; |
456 | 0 | term->suffix = 1; |
457 | 0 | end--; |
458 | 0 | } |
459 | 1.03k | if (start == end) |
460 | 0 | return (0); |
461 | | |
462 | 1.03k | if (term->inverse) |
463 | 140 | term->exact = 1; |
464 | 1.03k | term->text = start; |
465 | 1.03k | term->len = end - start; |
466 | 1.03k | return (1); |
467 | 1.03k | } |
468 | | |
469 | | /* Match one parsed term. */ |
470 | | static int |
471 | | fuzzy_match_term(const struct fuzzy_term *term, struct utf8_data *tok, |
472 | | struct fuzzy_char *cs, u_int ncs, int fold, int *score, char *matched) |
473 | 1.03k | { |
474 | 1.03k | u_int toklen; |
475 | 1.03k | int value = 0, matched_term; |
476 | | |
477 | 1.03k | toklen = fuzzy_decode(term->text, term->len, tok); |
478 | 1.03k | if (term->exact) { |
479 | 268 | matched_term = fuzzy_match_exact(tok, toklen, cs, ncs, fold, |
480 | 268 | term->prefix, term->suffix, &value, |
481 | 268 | term->inverse ? NULL : matched); |
482 | 762 | } else { |
483 | 762 | matched_term = fuzzy_match_fuzzy(tok, toklen, cs, ncs, fold, |
484 | 762 | &value, term->inverse ? NULL : matched); |
485 | 762 | } |
486 | | |
487 | 1.03k | if (term->inverse) |
488 | 140 | return (!matched_term); |
489 | 890 | if (!matched_term) |
490 | 885 | return (0); |
491 | 5 | *score += value; |
492 | 5 | return (1); |
493 | 890 | } |
494 | | |
495 | | /* Match one AND group of terms. */ |
496 | | static int |
497 | | fuzzy_match_group(const char *start, const char *end, struct utf8_data *tok, |
498 | | struct fuzzy_char *cs, u_int ncs, int fold, int *score, char *matched) |
499 | 1.03k | { |
500 | 1.03k | const char *cp = start, *sp; |
501 | 1.03k | struct fuzzy_term term; |
502 | 1.03k | int any = 0; |
503 | | |
504 | 1.03k | *score = 0; |
505 | 1.05k | while (cp != end) { |
506 | 1.04k | while (cp != end && *cp == ' ') |
507 | 2 | cp++; |
508 | 1.03k | if (cp == end) |
509 | 0 | break; |
510 | 1.03k | sp = cp; |
511 | 10.8k | while (cp != end && *cp != ' ') |
512 | 9.80k | cp++; |
513 | 1.03k | if (!fuzzy_parse_term(sp, cp, &term)) |
514 | 8 | return (0); |
515 | 1.03k | any = 1; |
516 | 1.03k | if (!fuzzy_match_term(&term, tok, cs, ncs, fold, score, |
517 | 1.03k | matched)) |
518 | 1.01k | return (0); |
519 | 1.03k | } |
520 | 15 | return (any); |
521 | 1.03k | } |
522 | | |
523 | | /* |
524 | | * Fuzzy match pattern against text, which is drawn into a region of the given |
525 | | * display width. Returns a bitstr_t of width bits with a bit set for each |
526 | | * column occupied by a matched character, or NULL if there is no match. A |
527 | | * higher returned score is better. |
528 | | */ |
529 | | bitstr_t * |
530 | | fuzzy_match(const char *pattern, const char *text, u_int width, u_int *score) |
531 | 908 | { |
532 | 908 | struct fuzzy_char *cs; |
533 | 908 | char *matched = NULL, *best = NULL, *groupmatched; |
534 | 908 | struct utf8_data *tok; |
535 | 908 | bitstr_t *mask; |
536 | 908 | u_int ncs, i, j, column; |
537 | 908 | u_int widths[STYLE_ALIGN_ABSOLUTE_CENTRE + 1]; |
538 | 908 | u_int start[STYLE_ALIGN_ABSOLUTE_CENTRE + 1]; |
539 | 908 | u_int src[STYLE_ALIGN_ABSOLUTE_CENTRE + 1]; |
540 | 908 | u_int vis[STYLE_ALIGN_ABSOLUTE_CENTRE + 1]; |
541 | 908 | u_int wl, wc, wr, wa; |
542 | 908 | const char *cp, *sp; |
543 | 908 | int bestscore = 0, groupscore, found = 0, fold; |
544 | | |
545 | 908 | if (width == 0) |
546 | 0 | return (NULL); |
547 | | |
548 | | /* An empty query matches everything, with nothing highlighted. */ |
549 | 908 | for (cp = pattern; *cp == ' ' || *cp == '|'; cp++) |
550 | 0 | /* nothing */; |
551 | 908 | if (*cp == '\0') { |
552 | 0 | if (score != NULL) |
553 | 0 | *score = 0; |
554 | 0 | return (bit_alloc(width)); |
555 | 0 | } |
556 | | |
557 | | /* Smart-case: fold unless the pattern has an uppercase character. */ |
558 | 908 | fold = 1; |
559 | 10.5k | for (cp = pattern; *cp != '\0'; cp++) { |
560 | 9.74k | if (*cp >= 'A' && *cp <= 'Z') { |
561 | 78 | fold = 0; |
562 | 78 | break; |
563 | 78 | } |
564 | 9.74k | } |
565 | | |
566 | | /* Scan the text into visible characters. */ |
567 | 908 | cs = fuzzy_scan(text, &ncs, widths); |
568 | 908 | matched = xcalloc(ncs == 0 ? 1 : ncs, sizeof *matched); |
569 | 908 | best = xcalloc(ncs == 0 ? 1 : ncs, sizeof *best); |
570 | 908 | tok = xreallocarray(NULL, strlen(pattern) + 1, sizeof *tok); |
571 | | |
572 | | /* Match each |-separated group and keep the best-scoring one. */ |
573 | 908 | cp = pattern; |
574 | 1.94k | while (*cp != '\0') { |
575 | 1.16k | while (*cp == ' ' || *cp == '|') |
576 | 128 | cp++; |
577 | 1.03k | if (*cp == '\0') |
578 | 0 | break; |
579 | 1.03k | sp = cp; |
580 | 10.9k | while (*cp != '\0' && *cp != '|') |
581 | 9.88k | cp++; |
582 | 1.03k | memset(matched, 0, ncs == 0 ? 1 : ncs); |
583 | 1.03k | groupmatched = matched; |
584 | 1.03k | if (fuzzy_match_group(sp, cp, tok, cs, ncs, fold, |
585 | 1.03k | &groupscore, groupmatched)) { |
586 | 15 | if (!found || groupscore > bestscore) { |
587 | 15 | found = 1; |
588 | 15 | bestscore = groupscore; |
589 | 15 | memcpy(best, matched, ncs == 0 ? 1 : ncs); |
590 | 15 | } |
591 | 15 | } |
592 | 1.03k | } |
593 | 908 | free(tok); |
594 | 908 | if (!found) { |
595 | 893 | free(best); |
596 | 893 | free(matched); |
597 | 893 | free(cs); |
598 | 893 | return (NULL); |
599 | 893 | } |
600 | | |
601 | | /* |
602 | | * Work out the trimmed widths and start columns of each alignment, |
603 | | * mirroring format_draw_none. |
604 | | */ |
605 | 15 | wl = widths[STYLE_ALIGN_LEFT]; |
606 | 15 | wc = widths[STYLE_ALIGN_CENTRE]; |
607 | 15 | wr = widths[STYLE_ALIGN_RIGHT]; |
608 | 15 | wa = widths[STYLE_ALIGN_ABSOLUTE_CENTRE]; |
609 | 98 | while (wl + wc + wr > width) { |
610 | 83 | if (wc > 0) |
611 | 0 | wc--; |
612 | 83 | else if (wr > 0) |
613 | 0 | wr--; |
614 | 83 | else |
615 | 83 | wl--; |
616 | 83 | } |
617 | 15 | if (wa > width) |
618 | 0 | wa = width; |
619 | | |
620 | 15 | start[STYLE_ALIGN_LEFT] = 0; |
621 | 15 | src[STYLE_ALIGN_LEFT] = 0; |
622 | 15 | vis[STYLE_ALIGN_LEFT] = wl; |
623 | | |
624 | 15 | start[STYLE_ALIGN_RIGHT] = width - wr; |
625 | 15 | src[STYLE_ALIGN_RIGHT] = widths[STYLE_ALIGN_RIGHT] - wr; |
626 | 15 | vis[STYLE_ALIGN_RIGHT] = wr; |
627 | | |
628 | 15 | start[STYLE_ALIGN_CENTRE] = |
629 | 15 | wl + ((width - wr) - wl) / 2 - wc / 2; |
630 | 15 | src[STYLE_ALIGN_CENTRE] = widths[STYLE_ALIGN_CENTRE] / 2 - wc / 2; |
631 | 15 | vis[STYLE_ALIGN_CENTRE] = wc; |
632 | | |
633 | 15 | start[STYLE_ALIGN_ABSOLUTE_CENTRE] = (width - wa) / 2; |
634 | 15 | src[STYLE_ALIGN_ABSOLUTE_CENTRE] = 0; |
635 | 15 | vis[STYLE_ALIGN_ABSOLUTE_CENTRE] = wa; |
636 | | |
637 | | /* Set a bit for each column of each matched character. */ |
638 | 15 | mask = bit_alloc(width); |
639 | 5.29k | for (i = 0; i < ncs; i++) { |
640 | 5.27k | if (!best[i]) |
641 | 5.26k | continue; |
642 | 15 | if (fuzzy_column(&cs[i], start, src, vis, &column) != 0) |
643 | 0 | continue; |
644 | 30 | for (j = 0; j < cs[i].width && column + j < width; j++) |
645 | 15 | bit_set(mask, column + j); |
646 | 15 | } |
647 | | |
648 | 15 | free(best); |
649 | 15 | free(matched); |
650 | 15 | free(cs); |
651 | | |
652 | 15 | if (score != NULL) |
653 | 0 | *score = (bestscore < 0) ? 0 : (u_int)bestscore; |
654 | 15 | return (mask); |
655 | 908 | } |