Coverage Report

Created: 2026-09-01 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tmux/fuzzy.c
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
1.83k
#define FUZZY_BONUS_EXACT 1000
54
852
#define FUZZY_BONUS_PREFIX 200
55
868
#define FUZZY_BONUS_SUFFIX 100
56
1.65k
#define FUZZY_BONUS_START 12
57
1.00k
#define FUZZY_BONUS_BOUNDARY 8
58
4.39k
#define FUZZY_BONUS_CONSECUTIVE 6
59
4.44k
#define FUZZY_PENALTY_LEADING 1
60
6.94k
#define FUZZY_PENALTY_LEADING_MAX 10
61
3.41k
#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
4.52k
{
85
4.52k
  static const char *boundary = " -_/.:";
86
87
4.52k
  if (ud->size != 1)
88
0
    return (0);
89
4.52k
  return (strchr(boundary, ud->data[0]) != NULL);
90
4.52k
}
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
674k
{
99
674k
  if (fold &&
100
545k
      a->size == 1 &&
101
545k
      b->size == 1 &&
102
545k
      a->data[0] < 0x80 &&
103
433k
      b->data[0] < 0x80)
104
433k
    return (tolower(a->data[0]) == tolower(b->data[0]));
105
240k
  return (a->size == b->size && memcmp(a->data, b->data, a->size) == 0);
106
674k
}
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
19.2k
{
112
19.2k
  if (align == STYLE_ALIGN_DEFAULT)
113
10.9k
    return (STYLE_ALIGN_LEFT);
114
8.25k
  return (align);
115
19.2k
}
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
775k
{
122
775k
  struct fuzzy_char *fc;
123
124
775k
  if (*ncs == *alloc) {
125
17.4k
    *alloc = (*alloc == 0) ? 64 : *alloc * 2;
126
17.4k
    *cs = xreallocarray(*cs, *alloc, sizeof **cs);
127
17.4k
  }
128
775k
  fc = &(*cs)[(*ncs)++];
129
775k
  fc->align = a;
130
775k
  memcpy(&fc->ud, ud, sizeof fc->ud);
131
775k
  fc->width = ud->width;
132
775k
  fc->offset = widths[a];
133
775k
  widths[a] += ud->width;
134
775k
}
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
1.07M
{
140
1.07M
  enum utf8_state  more;
141
1.07M
  const char  *start = cp;
142
143
1.07M
  if ((more = utf8_open(ud, (u_char)*cp)) == UTF8_MORE) {
144
220k
    while (++cp != end && more == UTF8_MORE)
145
132k
      more = utf8_append(ud, (u_char)*cp);
146
87.9k
    if (more == UTF8_DONE)
147
0
      return (cp);
148
87.9k
    cp = start;
149
87.9k
  }
150
1.07M
  utf8_set(ud, (u_char)*cp);
151
1.07M
  return (cp + 1);
152
1.07M
}
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
13.1k
{
162
13.1k
  struct fuzzy_char *cs = NULL;
163
13.1k
  u_int      alloc = 0, n, leading, i;
164
13.1k
  enum style_align   current = STYLE_ALIGN_LEFT;
165
13.1k
  struct style     sy;
166
13.1k
  const char    *cp = text, *textend = text + strlen(text);
167
13.1k
  const char    *end;
168
13.1k
  struct utf8_data   ud, hash, bracket;
169
13.1k
  char      *tmp;
170
171
13.1k
  *ncs = 0;
172
13.1k
  memset(widths, 0, sizeof *widths * (STYLE_ALIGN_ABSOLUTE_CENTRE + 1));
173
13.1k
  style_set(&sy, &grid_default_cell);
174
13.1k
  utf8_set(&hash, '#');
175
13.1k
  utf8_set(&bracket, '[');
176
177
1.04M
  while (*cp != '\0') {
178
    /* Handle a run of #s, which may introduce a style. */
179
1.03M
    if (*cp == '#') {
180
146k
      for (n = 0; cp[n] == '#'; n++)
181
81.3k
        /* nothing */;
182
65.3k
      if (cp[n] != '[') {
183
        /* Escaped #s: ##->#, so half (rounded up). */
184
31.2k
        leading = (n % 2 == 0) ? n / 2 : n / 2 + 1;
185
65.5k
        for (i = 0; i < leading; i++) {
186
34.3k
          fuzzy_add(&cs, ncs, &alloc, current,
187
34.3k
              &hash, widths);
188
34.3k
        }
189
31.2k
        cp += n;
190
31.2k
        continue;
191
31.2k
      }
192
193
      /* Even count: all #s escaped, the [ is literal. */
194
39.2k
      for (i = 0; i < n / 2; i++)
195
5.12k
        fuzzy_add(&cs, ncs, &alloc, current, &hash,
196
5.12k
            widths);
197
34.0k
      if (n % 2 == 0) {
198
1.35k
        fuzzy_add(&cs, ncs, &alloc, current, &bracket,
199
1.35k
            widths);
200
1.35k
        cp += n + 1;
201
1.35k
        continue;
202
1.35k
      }
203
204
      /* Odd count: this is a style, find and parse it. */
205
32.7k
      end = format_skip(cp + n + 1, "]");
206
32.7k
      if (end == NULL)
207
2.95k
        break;
208
29.7k
      tmp = xstrndup(cp + n + 1, end - (cp + n + 1));
209
29.7k
      if (style_parse(&sy, &grid_default_cell, tmp) == 0)
210
19.2k
        current = fuzzy_align(sy.align);
211
29.7k
      free(tmp);
212
29.7k
      cp = end + 1;
213
29.7k
      continue;
214
32.7k
    }
215
216
    /* Decode one character, multibyte or single byte. */
217
971k
    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
971k
    if (ud.size == 1 && (ud.data[0] <= 0x1f || ud.data[0] >= 0x7f))
225
236k
      continue;
226
734k
    fuzzy_add(&cs, ncs, &alloc, current, &ud, widths);
227
734k
  }
228
13.1k
  return (cs);
229
13.1k
}
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
6.46k
{
240
6.46k
  enum style_align  a = fc->align;
241
242
6.46k
  if (fc->offset < src[a] || fc->offset >= src[a] + vis[a])
243
3.26k
    return (-1);
244
3.20k
  *column = start[a] + (fc->offset - src[a]);
245
3.20k
  return (0);
246
6.46k
}
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
16.3k
{
252
16.3k
  const char  *cp = tok, *end = tok + len;
253
16.3k
  u_int    n = 0;
254
255
117k
  while (cp != end)
256
100k
    cp = fuzzy_decode_one(cp, end, &out[n++]);
257
16.3k
  return (n);
258
16.3k
}
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
3.41k
{
264
3.41k
  u_int i, gap, span;
265
3.41k
  int score = 0;
266
267
3.41k
  if (npos == 0)
268
0
    return (0);
269
3.41k
  if (pos[0] == 0)
270
805
    score += FUZZY_BONUS_START;
271
2.60k
  else {
272
2.60k
    if (fuzzy_is_boundary(&cs[pos[0] - 1].ud))
273
634
      score += FUZZY_BONUS_BOUNDARY;
274
2.60k
    if (pos[0] < FUZZY_PENALTY_LEADING_MAX)
275
1.06k
      score -= pos[0] * FUZZY_PENALTY_LEADING;
276
1.53k
    else {
277
1.53k
      score -= FUZZY_PENALTY_LEADING_MAX *
278
1.53k
          FUZZY_PENALTY_LEADING;
279
1.53k
    }
280
2.60k
  }
281
6.90k
  for (i = 1; i < npos; i++) {
282
3.49k
    if (pos[i] == pos[i - 1] + 1)
283
2.55k
      score += FUZZY_BONUS_CONSECUTIVE;
284
935
    else if (fuzzy_is_boundary(&cs[pos[i] - 1].ud))
285
203
      score += FUZZY_BONUS_BOUNDARY;
286
3.49k
  }
287
3.41k
  span = pos[npos - 1] - pos[0] + 1;
288
3.41k
  gap = span - npos;
289
3.41k
  score -= gap * FUZZY_PENALTY_GAP;
290
3.41k
  return (score);
291
3.41k
}
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
12.3k
{
301
12.3k
  u_int pi, ci, *pos;
302
12.3k
  int found, value;
303
304
12.3k
  if (toklen == 0 || ncs == 0)
305
35
    return (0);
306
12.3k
  pos = xcalloc(toklen, sizeof *pos);
307
308
  /* First find a subsequence from the start. */
309
12.3k
  ci = 0;
310
22.8k
  for (pi = 0; pi < toklen; pi++) {
311
588k
    while (ci != ncs &&
312
579k
        !fuzzy_char_equal(&tok[pi], &cs[ci].ud, fold))
313
568k
      ci++;
314
19.4k
    if (ci == ncs) {
315
8.94k
      free(pos);
316
8.94k
      return (0);
317
8.94k
    }
318
10.4k
    pos[pi] = ci++;
319
10.4k
  }
320
321
  /* Then compact it backwards to prefer a shorter span. */
322
3.41k
  ci = pos[toklen - 1];
323
10.3k
  for (pi = toklen; pi > 0; pi--) {
324
6.90k
    found = 0;
325
15.6k
    for (;;) {
326
15.6k
      if (fuzzy_char_equal(&tok[pi - 1], &cs[ci].ud, fold)) {
327
6.90k
        pos[pi - 1] = ci;
328
6.90k
        found = 1;
329
6.90k
        break;
330
6.90k
      }
331
8.72k
      if (ci == 0)
332
0
        break;
333
8.72k
      ci--;
334
8.72k
    }
335
6.90k
    if (!found) {
336
0
      free(pos);
337
0
      return (0);
338
0
    }
339
6.90k
    if (pi != 1)
340
3.49k
      ci--;
341
6.90k
  }
342
343
3.41k
  value = fuzzy_score_positions(pos, toklen, cs);
344
3.41k
  *score += value;
345
10.3k
  for (pi = 0; pi < toklen; pi++)
346
6.90k
    matched[pos[pi]] = 1;
347
3.41k
  free(pos);
348
3.41k
  return (1);
349
3.41k
}
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
1.83k
{
356
1.83k
  int score;
357
358
1.83k
  score = FUZZY_BONUS_EXACT + toklen * FUZZY_BONUS_CONSECUTIVE;
359
1.83k
  if (prefix)
360
852
    score += FUZZY_BONUS_PREFIX;
361
1.83k
  if (suffix)
362
868
    score += FUZZY_BONUS_SUFFIX;
363
1.83k
  if (start == 0)
364
854
    score += FUZZY_BONUS_START;
365
982
  else if (fuzzy_is_boundary(&cs[start - 1].ud))
366
169
    score += FUZZY_BONUS_BOUNDARY;
367
1.83k
  if (start < FUZZY_PENALTY_LEADING_MAX)
368
868
    score -= start * FUZZY_PENALTY_LEADING;
369
968
  else
370
968
    score -= FUZZY_PENALTY_LEADING_MAX * FUZZY_PENALTY_LEADING;
371
1.83k
  if (!prefix && !suffix)
372
968
    score -= ncs - (start + toklen);
373
1.83k
  return (score);
374
1.83k
}
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
3.94k
{
382
3.94k
  u_int start, end, i, j, best = 0;
383
3.94k
  int ok, found = 0, value, bestscore = 0;
384
385
3.94k
  if (toklen == 0 || toklen > ncs)
386
1.19k
    return (0);
387
388
2.74k
  if (prefix && suffix) {
389
1.40k
    if (toklen != ncs)
390
421
      return (0);
391
984
    start = 0;
392
984
    end = 1;
393
1.34k
  } else if (prefix) {
394
633
    start = 0;
395
633
    end = 1;
396
710
  } else if (suffix) {
397
41
    start = ncs - toklen;
398
41
    end = start + 1;
399
669
  } else {
400
669
    start = 0;
401
669
    end = ncs - toklen + 1;
402
669
  }
403
404
77.9k
  for (i = start; i < end; i++) {
405
75.5k
    ok = 1;
406
81.0k
    for (j = 0; j < toklen; j++) {
407
79.2k
      if (!fuzzy_char_equal(&tok[j], &cs[i + j].ud, fold)) {
408
73.7k
        ok = 0;
409
73.7k
        break;
410
73.7k
      }
411
79.2k
    }
412
75.5k
    if (!ok)
413
73.7k
      continue;
414
1.83k
    value = fuzzy_score_exact(i, toklen, ncs, cs, prefix, suffix);
415
1.83k
    if (!found || value > bestscore) {
416
1.81k
      found = 1;
417
1.81k
      best = i;
418
1.81k
      bestscore = value;
419
1.81k
    }
420
1.83k
  }
421
2.32k
  if (!found)
422
998
    return (0);
423
1.32k
  *score += bestscore;
424
1.32k
  if (matched != NULL) {
425
2.26k
    for (i = 0; i < toklen; i++)
426
1.21k
      matched[best + i] = 1;
427
1.04k
  }
428
1.32k
  return (1);
429
2.32k
}
430
431
/* Parse one term. */
432
static int
433
fuzzy_parse_term(const char *start, const char *end, struct fuzzy_term *term)
434
17.3k
{
435
17.3k
  memset(term, 0, sizeof *term);
436
17.3k
  if (start == end)
437
0
    return (0);
438
17.3k
  if (*start == '!') {
439
1.53k
    term->inverse = 1;
440
1.53k
    start++;
441
1.53k
  }
442
17.3k
  if (start == end)
443
401
    return (0);
444
16.9k
  if (*start == '\'') {
445
306
    term->exact = 1;
446
306
    start++;
447
16.6k
  } else if (*start == '^') {
448
2.30k
    term->exact = 1;
449
2.30k
    term->prefix = 1;
450
2.30k
    start++;
451
2.30k
  }
452
16.9k
  if (start == end)
453
128
    return (0);
454
16.8k
  if (end[-1] == '$') {
455
2.21k
    term->exact = 1;
456
2.21k
    term->suffix = 1;
457
2.21k
    end--;
458
2.21k
  }
459
16.8k
  if (start == end)
460
474
    return (0);
461
462
16.3k
  if (term->inverse)
463
1.13k
    term->exact = 1;
464
16.3k
  term->text = start;
465
16.3k
  term->len = end - start;
466
16.3k
  return (1);
467
16.8k
}
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
16.3k
{
474
16.3k
  u_int toklen;
475
16.3k
  int value = 0, matched_term;
476
477
16.3k
  toklen = fuzzy_decode(term->text, term->len, tok);
478
16.3k
  if (term->exact) {
479
3.94k
    matched_term = fuzzy_match_exact(tok, toklen, cs, ncs, fold,
480
3.94k
        term->prefix, term->suffix, &value,
481
3.94k
        term->inverse ? NULL : matched);
482
12.3k
  } else {
483
12.3k
    matched_term = fuzzy_match_fuzzy(tok, toklen, cs, ncs, fold,
484
12.3k
        &value, term->inverse ? NULL : matched);
485
12.3k
  }
486
487
16.3k
  if (term->inverse)
488
1.13k
    return (!matched_term);
489
15.2k
  if (!matched_term)
490
10.7k
    return (0);
491
4.45k
  *score += value;
492
4.45k
  return (1);
493
15.2k
}
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
16.3k
{
500
16.3k
  const char    *cp = start, *sp;
501
16.3k
  struct fuzzy_term  term;
502
16.3k
  int      any = 0;
503
504
16.3k
  *score = 0;
505
21.6k
  while (cp != end) {
506
22.0k
    while (cp != end && *cp == ' ')
507
3.96k
      cp++;
508
18.0k
    if (cp == end)
509
709
      break;
510
17.3k
    sp = cp;
511
124k
    while (cp != end && *cp != ' ')
512
107k
      cp++;
513
17.3k
    if (!fuzzy_parse_term(sp, cp, &term))
514
1.00k
      return (0);
515
16.3k
    any = 1;
516
16.3k
    if (!fuzzy_match_term(&term, tok, cs, ncs, fold, score,
517
16.3k
        matched))
518
11.0k
      return (0);
519
16.3k
  }
520
4.27k
  return (any);
521
16.3k
}
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
13.1k
{
532
13.1k
  struct fuzzy_char *cs;
533
13.1k
  char      *matched = NULL, *best = NULL, *groupmatched;
534
13.1k
  struct utf8_data  *tok;
535
13.1k
  bitstr_t    *mask;
536
13.1k
  u_int      ncs, i, j, column;
537
13.1k
  u_int      widths[STYLE_ALIGN_ABSOLUTE_CENTRE + 1];
538
13.1k
  u_int      start[STYLE_ALIGN_ABSOLUTE_CENTRE + 1];
539
13.1k
  u_int      src[STYLE_ALIGN_ABSOLUTE_CENTRE + 1];
540
13.1k
  u_int      vis[STYLE_ALIGN_ABSOLUTE_CENTRE + 1];
541
13.1k
  u_int      wl, wc, wr, wa;
542
13.1k
  const char    *cp, *sp;
543
13.1k
  int      bestscore = 0, groupscore, found = 0, fold;
544
545
13.1k
  if (width == 0)
546
0
    return (NULL);
547
548
  /* An empty query matches everything, with nothing highlighted. */
549
13.7k
  for (cp = pattern; *cp == ' ' || *cp == '|'; cp++)
550
681
    /* nothing */;
551
13.1k
  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
13.1k
  fold = 1;
559
84.3k
  for (cp = pattern; *cp != '\0'; cp++) {
560
73.1k
    if (*cp >= 'A' && *cp <= 'Z') {
561
1.92k
      fold = 0;
562
1.92k
      break;
563
1.92k
    }
564
73.1k
  }
565
566
  /* Scan the text into visible characters. */
567
13.1k
  cs = fuzzy_scan(text, &ncs, widths);
568
13.1k
  matched = xcalloc(ncs == 0 ? 1 : ncs, sizeof *matched);
569
13.1k
  best = xcalloc(ncs == 0 ? 1 : ncs, sizeof *best);
570
13.1k
  tok = xreallocarray(NULL, strlen(pattern) + 1, sizeof *tok);
571
572
  /* Match each |-separated group and keep the best-scoring one. */
573
13.1k
  cp = pattern;
574
29.4k
  while (*cp != '\0') {
575
22.3k
    while (*cp == ' ' || *cp == '|')
576
5.83k
      cp++;
577
16.5k
    if (*cp == '\0')
578
226
      break;
579
16.3k
    sp = cp;
580
147k
    while (*cp != '\0' && *cp != '|')
581
130k
      cp++;
582
16.3k
    memset(matched, 0, ncs == 0 ? 1 : ncs);
583
16.3k
    groupmatched = matched;
584
16.3k
    if (fuzzy_match_group(sp, cp, tok, cs, ncs, fold,
585
16.3k
        &groupscore, groupmatched)) {
586
4.27k
      if (!found || groupscore > bestscore) {
587
3.94k
        found = 1;
588
3.94k
        bestscore = groupscore;
589
3.94k
        memcpy(best, matched, ncs == 0 ? 1 : ncs);
590
3.94k
      }
591
4.27k
    }
592
16.3k
  }
593
13.1k
  free(tok);
594
13.1k
  if (!found) {
595
9.19k
    free(best);
596
9.19k
    free(matched);
597
9.19k
    free(cs);
598
9.19k
    return (NULL);
599
9.19k
  }
600
601
  /*
602
   * Work out the trimmed widths and start columns of each alignment,
603
   * mirroring format_draw_none.
604
   */
605
3.91k
  wl = widths[STYLE_ALIGN_LEFT];
606
3.91k
  wc = widths[STYLE_ALIGN_CENTRE];
607
3.91k
  wr = widths[STYLE_ALIGN_RIGHT];
608
3.91k
  wa = widths[STYLE_ALIGN_ABSOLUTE_CENTRE];
609
109k
  while (wl + wc + wr > width) {
610
105k
    if (wc > 0)
611
0
      wc--;
612
105k
    else if (wr > 0)
613
2.41k
      wr--;
614
103k
    else
615
103k
      wl--;
616
105k
  }
617
3.91k
  if (wa > width)
618
0
    wa = width;
619
620
3.91k
  start[STYLE_ALIGN_LEFT] = 0;
621
3.91k
  src[STYLE_ALIGN_LEFT] = 0;
622
3.91k
  vis[STYLE_ALIGN_LEFT] = wl;
623
624
3.91k
  start[STYLE_ALIGN_RIGHT] = width - wr;
625
3.91k
  src[STYLE_ALIGN_RIGHT] = widths[STYLE_ALIGN_RIGHT] - wr;
626
3.91k
  vis[STYLE_ALIGN_RIGHT] = wr;
627
628
3.91k
  start[STYLE_ALIGN_CENTRE] =
629
3.91k
      wl + ((width - wr) - wl) / 2 - wc / 2;
630
3.91k
  src[STYLE_ALIGN_CENTRE] = widths[STYLE_ALIGN_CENTRE] / 2 - wc / 2;
631
3.91k
  vis[STYLE_ALIGN_CENTRE] = wc;
632
633
3.91k
  start[STYLE_ALIGN_ABSOLUTE_CENTRE] = (width - wa) / 2;
634
3.91k
  src[STYLE_ALIGN_ABSOLUTE_CENTRE] = 0;
635
3.91k
  vis[STYLE_ALIGN_ABSOLUTE_CENTRE] = wa;
636
637
  /* Set a bit for each column of each matched character. */
638
3.91k
  mask = bit_alloc(width);
639
218k
  for (i = 0; i < ncs; i++) {
640
214k
    if (!best[i])
641
208k
      continue;
642
6.46k
    if (fuzzy_column(&cs[i], start, src, vis, &column) != 0)
643
3.26k
      continue;
644
6.41k
    for (j = 0; j < cs[i].width && column + j < width; j++)
645
3.20k
      bit_set(mask, column + j);
646
3.20k
  }
647
648
3.91k
  free(best);
649
3.91k
  free(matched);
650
3.91k
  free(cs);
651
652
3.91k
  if (score != NULL)
653
0
    *score = (bestscore < 0) ? 0 : (u_int)bestscore;
654
3.91k
  return (mask);
655
13.1k
}