Coverage Report

Created: 2026-07-16 06:55

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
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
}