NeoMutt
2025-12-11-1009-ga75d9e
Teaching an old dog new tricks
DOXYGEN
Toggle main menu visibility
Loading...
Searching...
No Matches
subseq.c
Go to the documentation of this file.
1
22
125
126
#include "config.h"
127
#include <stdbool.h>
128
#include <string.h>
129
#include "
lib.h
"
130
#include "
fuzzy.h
"
131
133
#define DEFAULT_MAX_PATTERN 256
134
144
static
inline
unsigned
char
ascii_tolower
(
unsigned
char
c)
145
{
146
if
((c >=
'A'
) && (c <=
'Z'
))
147
return
c + (
'a'
-
'A'
);
148
return
c;
149
}
150
160
static
inline
int
lower_if
(
int
c,
bool
fold)
161
{
162
return
fold ?
ascii_tolower
((
unsigned
char
) c) : c;
163
}
164
171
static
inline
bool
utf8_is_continuation
(
unsigned
char
c)
172
{
173
return
(c & 0xC0) == 0x80;
174
}
175
183
static
int
utf8_char_len
(
const
char
*s)
184
{
185
const
unsigned
char
c0 = (
unsigned
char) s[0];
186
187
if
(c0 < 0x80)
188
return
1;
189
190
if
((c0 >= 0xC2) && (c0 <= 0xDF))
191
{
192
if
(s[1] &&
utf8_is_continuation
((
unsigned
char
) s[1]))
193
return
2;
194
return
1;
195
}
196
197
if
((c0 >= 0xE0) && (c0 <= 0xEF))
198
{
199
if
(s[1] && s[2] &&
utf8_is_continuation
((
unsigned
char
) s[1]) &&
200
utf8_is_continuation
((
unsigned
char
) s[2]))
201
return
3;
202
return
1;
203
}
204
205
if
((c0 >= 0xF0) && (c0 <= 0xF4))
206
{
207
if
(s[1] && s[2] && s[3] &&
utf8_is_continuation
((
unsigned
char
) s[1]) &&
208
utf8_is_continuation
((
unsigned
char
) s[2]) &&
209
utf8_is_continuation
((
unsigned
char
) s[3]))
210
return
4;
211
return
1;
212
}
213
214
return
1;
215
}
216
227
static
bool
compute_case_mode
(
const
char
*pattern,
const
struct
FuzzyOptions
*opts)
228
{
229
if
(!opts)
230
return
true
;
// default case-insensitive
231
232
if
(opts->
case_sensitive
)
233
return
false
;
234
235
if
(opts->
smart_case
)
236
{
237
// Check if pattern contains any ASCII uppercase (A-Z)
238
for
(
const
char
*p = pattern; *p; p++)
239
{
240
unsigned
char
c = (
unsigned
char) *p;
241
if
((c >=
'A'
) && (c <=
'Z'
))
242
return
false
;
// found uppercase, use case-sensitive
243
}
244
}
245
246
return
true
;
// fold case
247
}
248
262
int
fuzzy_subseq_match
(
const
char
*pattern,
const
char
*
candidate
,
263
const
struct
FuzzyOptions
*opts,
struct
FuzzyResult
*out)
264
{
265
if
(!pattern || !
candidate
)
266
return
-1;
267
268
size_t
plen = strlen(pattern);
269
if
(plen == 0)
270
return
-1;
271
272
int
max_pattern = (opts && opts->
max_pattern
) ? opts->
max_pattern
:
DEFAULT_MAX_PATTERN
;
273
if
((max_pattern <= 0) || (max_pattern >
DEFAULT_MAX_PATTERN
))
274
max_pattern =
DEFAULT_MAX_PATTERN
;
275
276
if
(plen > (
size_t
) max_pattern)
277
return
-1;
278
279
bool
fold =
compute_case_mode
(pattern, opts);
280
281
int
matchpos[
DEFAULT_MAX_PATTERN
] = { 0 };
282
283
int
pi = 0;
284
int
ci = 0;
285
int
score = 0;
286
287
int
first = -1;
288
int
last = -1;
289
290
// Forward subsequence scan
291
while
(
candidate
[ci] && pi < (
int
) plen)
292
{
293
const
unsigned
char
pbyte = (
unsigned
char) pattern[pi];
294
if
(pbyte < 0x80)
295
{
296
int
pc =
lower_if
(pattern[pi], fold);
297
int
cc =
lower_if
(
candidate
[ci], fold);
298
299
if
(pc == cc)
300
{
301
matchpos[pi] = ci;
302
303
if
(first < 0)
304
first = ci;
305
306
last = ci;
307
pi++;
308
}
309
310
ci++;
311
continue
;
312
}
313
314
// Non-ASCII pattern bytes must match as a whole UTF-8 codepoint.
315
const
int
pchar_len =
utf8_char_len
(pattern + pi);
316
const
int
premaining = (int) plen - pi;
317
if
((pchar_len <= 0) || (pchar_len > premaining))
318
return
-1;
319
bool
matched =
false
;
320
321
while
(
candidate
[ci])
322
{
323
const
unsigned
char
cbyte = (
unsigned
char)
candidate
[ci];
324
if
(
utf8_is_continuation
(cbyte))
325
{
326
ci++;
327
continue
;
328
}
329
330
const
int
cchar_len =
utf8_char_len
(
candidate
+ ci);
331
if
((pchar_len == cchar_len) && (memcmp(pattern + pi,
candidate
+ ci, pchar_len) == 0))
332
{
333
for
(
int
k = 0; k < pchar_len; k++)
334
{
335
const
int
match_idx = pi + k;
336
if
(match_idx >=
DEFAULT_MAX_PATTERN
)
337
return
-1;
338
matchpos[match_idx] = ci + k;
339
}
340
341
if
(first < 0)
342
first = ci;
343
344
last = ci + pchar_len - 1;
345
pi += pchar_len;
346
ci += cchar_len;
347
matched =
true
;
348
break
;
349
}
350
351
ci += cchar_len;
352
}
353
354
if
(!matched)
355
break
;
356
}
357
358
if
(pi != (
int
) plen)
359
return
-1;
// not a subsequence
360
361
// Scoring
362
363
// Base score
364
score += plen * 10;
365
366
// Consecutive & gap penalties
367
for
(
int
i = 1; i < pi; i++)
368
{
369
int
gap = matchpos[i] - matchpos[i - 1] - 1;
370
371
if
(gap == 0)
372
score += 15;
// compact match bonus
373
else
374
score -= gap * 2;
375
}
376
377
// Span penalty
378
int
span = last - first + 1;
379
score -= span;
380
381
// Prefix bonus
382
if
(first == 0 && opts && opts->
prefer_prefix
)
383
score += 40;
384
385
// Boundary bonus (ASCII-only separators and CamelCase)
386
for
(
int
i = 0; i < pi; i++)
387
{
388
int
pos = matchpos[i];
389
390
if
(pos == 0)
391
score += 30;
// start of string
392
else
393
{
394
unsigned
char
prev = (
unsigned
char)
candidate
[pos - 1];
395
unsigned
char
curr = (
unsigned
char)
candidate
[pos];
396
397
// ASCII separator boundaries
398
if
((prev ==
'/'
) || (prev ==
'.'
) || (prev ==
'-'
) || (prev ==
'_'
))
399
score += 15;
400
// ASCII CamelCase boundary (lowercase followed by uppercase)
401
else
if
(((prev >=
'a'
) && (prev <=
'z'
)) && ((curr >=
'A'
) && (curr <=
'Z'
)))
402
score += 10;
403
}
404
}
405
406
// Mild length penalty
407
score -= (int) strlen(
candidate
) / 4;
408
409
// Ensure valid matches always return non-negative score
410
if
(score < 0)
411
score = 0;
412
413
if
(out)
414
{
415
out->
score
= score;
416
out->
span
= span;
417
out->
start
= first;
418
out->
end
= last;
419
}
420
421
return
score;
422
}
candidate
bool candidate(struct CompletionData *cd, char *user, const char *src, char *dest, size_t dlen)
Helper function for completion.
Definition
helpers.c:79
lib.h
Fuzzy matching library.
fuzzy.h
Fuzzy matching library - private definitions.
FuzzyOptions
Options for fuzzy matching.
Definition
lib.h:92
FuzzyOptions::smart_case
bool smart_case
Auto case-sensitive if pattern has uppercase.
Definition
lib.h:94
FuzzyOptions::case_sensitive
bool case_sensitive
Match case exactly.
Definition
lib.h:93
FuzzyOptions::max_pattern
int max_pattern
Safety bound (<=0 = default 256, capped at 256).
Definition
lib.h:96
FuzzyOptions::prefer_prefix
bool prefer_prefix
Extra weight for prefix matches.
Definition
lib.h:95
FuzzyResult
Result of a fuzzy match.
Definition
lib.h:103
FuzzyResult::score
int score
Score (<0 = no match).
Definition
lib.h:104
FuzzyResult::span
int span
Match span.
Definition
lib.h:105
FuzzyResult::start
int start
First match position.
Definition
lib.h:106
FuzzyResult::end
int end
Last match position.
Definition
lib.h:107
fuzzy_subseq_match
int fuzzy_subseq_match(const char *pattern, const char *candidate, const struct FuzzyOptions *opts, struct FuzzyResult *out)
Perform subsequence fuzzy matching (UTF-8 aware, ASCII case-folding).
Definition
subseq.c:262
utf8_char_len
static int utf8_char_len(const char *s)
Get length of a UTF-8 codepoint at a byte offset.
Definition
subseq.c:183
utf8_is_continuation
static bool utf8_is_continuation(unsigned char c)
Check for UTF-8 continuation byte.
Definition
subseq.c:171
ascii_tolower
static unsigned char ascii_tolower(unsigned char c)
Convert ASCII character to lowercase.
Definition
subseq.c:144
lower_if
static int lower_if(int c, bool fold)
Convert character to lowercase conditionally.
Definition
subseq.c:160
compute_case_mode
static bool compute_case_mode(const char *pattern, const struct FuzzyOptions *opts)
Determine if case folding should be used.
Definition
subseq.c:227
DEFAULT_MAX_PATTERN
#define DEFAULT_MAX_PATTERN
Default maximum pattern length.
Definition
subseq.c:133