branch: externals/hotfuzz
commit 188e2c6c23774614054f8168fca0bb1199c03e0a
Author: Axel Forsman <[email protected]>
Commit: Axel Forsman <[email protected]>
Simplify the C implementation
* Use conventional fat pointer string slices instead of prepending the
string data with the Emacs Lisp value and length.
* Try to copy_string_contents into the remaining buffer instead of
always calling it once initially to get the required capacity.
* Always pad copied Emacs strings to 8 bytes boundaries to remove the
strtolower special cases.
* Use a signed atomic counter instead of protecting it with a mutex.
This makes the worker routine infallible. Also, skip assigning an
initial batch to each worker and let them fetch it themselves.
---
hotfuzz-module.c | 348 ++++++++++++++++++++-----------------------------------
hotfuzz.el | 36 +++---
2 files changed, 143 insertions(+), 241 deletions(-)
diff --git a/hotfuzz-module.c b/hotfuzz-module.c
index e55af5e575..145f1de161 100644
--- a/hotfuzz-module.c
+++ b/hotfuzz-module.c
@@ -12,8 +12,7 @@
#include <pthread.h>
#include <unistd.h>
-#define MIN(a, b) ({ __typeof__(a) _a = (a), _b = (b); _a < _b ? _a : _b; })
-#define MAX(a, b) ({ __typeof__(a) _a = (a), _b = (b); _a > _b ? _a : _b; })
+#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#define MAX_NEEDLE_LEN 128
#define MAX_HAYSTACK_LEN 512
@@ -21,52 +20,25 @@
int plugin_is_GPL_compatible;
-/** An Emacs string made accessible by copying. */
-struct EmacsStr {
- emacs_value value; ///< The original string value.
- size_t len; ///< The length of the string minus the null byte.
- char b[]; ///< The null-terminated copied string.
-};
-
-static char tolower_utf8(char c) {
- return *u8"A" <= c && c <= *u8"Z" ? c + (*u8"a" - *u8"A") : c;
-}
+struct Str { char *b; size_t len; };
static char toupper_utf8(char c) {
return *u8"a" <= c && c <= *u8"z" ? c - (*u8"a" - *u8"A") : c;
}
-
-static uint64_t tolower8(uint64_t x) {
- uint64_t ones = 0x0101010101010101,
- is_gt_Z = (0x7f * ones & x) + (0x7f - *u8"Z") * ones,
- is_ge_A = (0x7f * ones & x) + (0x80 - *u8"A") * ones,
- is_upper = 0x80 * ones & ~x & (is_ge_A ^ is_gt_Z);
- return x | is_upper >> 2;
-}
-
-static void strtolower(struct EmacsStr *s) {
- // Complicated in order to optimize out the calls to tolower_utf8
- // on AMD64 System V with GCC 11.3.0.
-
- size_t i = 0;
- if ((alignof(struct EmacsStr) + offsetof(struct EmacsStr, b)) %
alignof(uint64_t))
- for (; ((uintptr_t) s->b + i) % alignof(uint64_t) && i <
s->len; ++i)
- s->b[i] = tolower_utf8(s->b[i]);
-
- size_t pad = alignof(struct EmacsStr)
- - offsetof(struct EmacsStr, b) % alignof(struct EmacsStr);
- for (; i + 8 < s->len + MIN(pad, 8); i += 8) {
- uint64_t *x = (uint64_t *) (s->b + i);
- *x = tolower8(*x);
+static void strtolower(struct Str s) {
+ uint64_t ones = ~UINT64_C(0) / 0xff, x;
+ for (size_t i = 0; i < s.len; i += sizeof x) {
+ memcpy(&x, s.b, sizeof x);
+ uint64_t is_gt_Z = (0x7f * ones & x) + (0x7f - *u8"Z") * ones,
+ is_ge_A = (0x7f * ones & x) + (0x80 - *u8"A") * ones,
+ is_upper = 0x80 * ones & ~x & (is_ge_A ^ is_gt_Z);
+ x |= is_upper >> 2;
+ memcpy(s.b, &x, sizeof x);
}
-
- if (pad < 8 - 1) for (; i < s->len; ++i) s->b[i] =
tolower_utf8(s->b[i]);
}
-typedef int cost;
-
-static cost char_bonus(char prev, char ch) {
- cost word_bonus = 80;
+static int char_bonus(char prev, char ch) {
+ int word_bonus = 80;
switch (ch) {
case 'A' ... 'Z':
if ('a' <= prev && prev <= 'z') return word_bonus;
@@ -83,86 +55,59 @@ static cost char_bonus(char prev, char ch) {
}
}
-static void calc_bonus(struct EmacsStr *haystack, cost *out) {
- char ch, lastch = '/';
- for (size_t i = 0; i < haystack->len; ++i, lastch = ch)
- out[i] = char_bonus(lastch, ch = haystack->b[i]);
-}
-
-static void match_row(struct EmacsStr *a, struct EmacsStr *b, cost *bonuses,
unsigned i,
- cost *nc, cost *nd, cost *pc, cost *pd) {
- cost g = 100, h = 5;
- size_t m = b->len;
- cost oldc, s = i ? g + h * i : 0;
+static void match_row(struct Str a, struct Str b, int *bonuses, unsigned i,
int *c, int *d) {
+ int g = 100, h = 5;
+ size_t m = b.len;
+ int oldc, s = i ? g + h * i : 0;
for (size_t j = 0; j < m; ++j, s = oldc) {
- oldc = pc[j];
- nc[j] = MIN(nd[j] = MIN(pd[j], oldc + g) + (j == m - 1 ? h : 2
* h),
- a->b[i] == b->b[j] ? s - bonuses[i] : 100000);
+ oldc = c[j];
+ d[j] = MIN(d[j], oldc + g) + (j == m - 1 ? h : 2 * h);
+ c[j] = MIN(d[j], a.b[i] == b.b[j] ? s - bonuses[i] : 100000);
}
}
-static cost get_cost(struct EmacsStr *needle, struct EmacsStr *haystack, bool
ignore_case) {
- unsigned n = haystack->len, m = needle->len;
- if (n > MAX_HAYSTACK_LEN || m > MAX_NEEDLE_LEN) return 10000;
- cost c[MAX_NEEDLE_LEN], d[MAX_NEEDLE_LEN];
- for (unsigned j = 0; j < m; ++j) c[j] = d[j] = 10000;
+static int calc_cost(struct Str needle, struct Str haystack, bool ignore_case)
{
+ unsigned n = haystack.len, m = needle.len;
+ if (n > MAX_HAYSTACK_LEN || m > MAX_NEEDLE_LEN) return 100000;
+ int c[MAX_NEEDLE_LEN], d[MAX_NEEDLE_LEN];
+ for (unsigned j = 0; j < m; ++j) c[j] = d[j] = 100000;
- cost bonuses[MAX_HAYSTACK_LEN];
- calc_bonus(haystack, bonuses);
+ int bonuses[MAX_HAYSTACK_LEN];
+ char ch, lastch = '/';
+ for (size_t i = 0; i < n; ++i, lastch = ch)
+ bonuses[i] = char_bonus(lastch, ch = haystack.b[i]);
if (ignore_case) strtolower(haystack);
for (unsigned i = 0; i < n; ++i)
- match_row(haystack, needle, bonuses, i, c, d, c, d);
+ match_row(haystack, needle, bonuses, i, c, d);
return c[m - 1];
}
/**
- * Returns whether @p haystack matches @p needle.
+ * Returns whether @a haystack matches @a needle.
*
* @param needle Null-terminated search string.
* @param haystack Null-terminated completion candidate.
* @param ignore_case Whether to match case-insensitively.
*/
static bool is_match(char *needle, char *haystack, bool ignore_case) {
- while (*needle) {
+ while (*needle)
if (ignore_case
? (haystack = strpbrk(haystack, (char[]) { *needle,
toupper_utf8(*needle), '\0' }))
: (haystack = strchr(haystack, *needle)))
++needle, ++haystack; // Skip past matched character
else
return false;
- }
return true;
}
/** Intrusive linked list of bump allocation blocks. */
struct Bump {
struct Bump *next;
- size_t index, capacity;
- char b[];
+ char *cursor, *limit, b[];
};
-/**
- * Allocates the specified number of bytes.
- *
- * Returns NULL on failure.
- */
-static void *bump_alloc(struct Bump **head, size_t len) {
- if (!*head || (*head)->capacity - (*head)->index < len) {
- size_t capacity = MAX(*head ? 2 * (*head)->capacity : 1024,
len);
- struct Bump *new_head;
- if (!(new_head = malloc(sizeof *new_head + capacity)))
- return NULL;
- *new_head = (struct Bump) { .next = *head, .index = 0,
.capacity = capacity };
- *head = new_head;
- }
-
- void *p = (*head)->b + (*head)->index;
- (*head)->index += len;
- return p;
-}
-
static void bump_free(struct Bump *head) {
while (head) {
struct Bump *next = head->next;
@@ -171,31 +116,43 @@ static void bump_free(struct Bump *head) {
}
}
-/**
- * Copies the Emacs string to make its lifetime that of the allocator.
- */
-static struct EmacsStr *copy_emacs_string(emacs_env *env, struct Bump **bump,
emacs_value value) {
- ptrdiff_t len;
+/** Copies the Emacs string to make its contents accessible. */
+static struct Str copy_emacs_string(emacs_env *env, struct Bump **bump,
emacs_value value) {
+ char *buf = NULL;
+ ptrdiff_t origlen, len;
+ if (*bump) {
+ // Opportunistically try to copy into remaining space
+ buf = (*bump)->cursor;
+ len = origlen = (*bump)->limit - (*bump)->cursor;
+ }
// Determine the size of the string (including null-terminator)
- if (!env->copy_string_contents(env, value, NULL, &len))
- return NULL;
-
- struct EmacsStr *result;
- // Note: Since only EmacsStr:s are allocated with bump_alloc we
- // may use its smaller alignment rather than the scalar maximum.
- if (!(result = bump_alloc(bump, sizeof *result + len
- + alignof(struct EmacsStr) - 1 &
~(alignof(struct EmacsStr) - 1))))
- return NULL;
-
- result->value = value;
- result->len = len - 1;
- env->copy_string_contents(env, value, result->b, &len);
- return result;
+ if (env->copy_string_contents(env, value, buf, &len)) {
+ if (buf) goto success;
+ } else {
+ if (!buf || len == origlen) return (struct Str) { 0 };
+ env->non_local_exit_clear(env);
+ }
+
+ size_t capacity = *bump ? 2 * ((*bump)->limit - (*bump)->b) : 2048;
+ if (capacity < (size_t) len) capacity = len + alignof(uint64_t) - 1;
+ struct Bump *new;
+ if (!(new = malloc(sizeof *new + capacity))) return (struct Str) { 0 };
+ *new = (struct Bump) { .next = *bump, .cursor = new->b, .limit = new->b
+ capacity };
+ *bump = new;
+
+ env->copy_string_contents(env, value, buf = new->cursor, &len);
+success:
+ (*bump)->cursor = (char *) (((uintptr_t) (*bump)->cursor + len
+ + alignof(uint64_t) - 1) & ~(alignof(uint64_t) - 1));
+ return (struct Str) { buf, len - 1 };
}
struct Candidate {
- struct EmacsStr *s;
- cost key;
+ emacs_value value;
+ union {
+ struct Str s;
+ int key;
+ };
};
static int cmp_candidate(const void *a, const void *b) {
@@ -208,139 +165,95 @@ struct Batch {
};
struct Shared {
- pthread_mutex_t mutex;
- bool ignore_case;
- struct EmacsStr *needle;
- struct Batch *batches, *batches_end;
-};
-
-struct Worker {
- pthread_t thread;
- struct Shared *shared;
- struct Batch *batch; ///< The initial batch to work on.
+ const bool ignore_case;
+ const struct Str needle;
+ struct Batch *const batches;
+ _Atomic ssize_t remaining;
};
-static enum JobRetVal {
- JOB_FINISHED,
- JOB_FAILED
-} job_finished = JOB_FINISHED, job_failed = JOB_FAILED;
-
static void *worker_routine(void *ptr) {
- struct Worker *worker = ptr;
- struct Shared *shared = worker->shared;
- struct EmacsStr *needle = shared->needle;
- struct Batch *batch = worker->batch;
+ struct Shared *shared = ptr;
+ struct Str needle = shared->needle;
- do {
- unsigned num_matches = 0;
+ ssize_t batch_idx;
+ while ((batch_idx = --shared->remaining) >= 0) {
+ struct Batch *batch = shared->batches + batch_idx;
+ unsigned n = 0;
for (unsigned i = 0; i < batch->len; ++i) {
- struct Candidate *candidate = batch->xs + i;
- if (!is_match(needle->b, candidate->s->b,
shared->ignore_case)) continue;
- batch->xs[num_matches++] = (struct Candidate) {
- .s = candidate->s,
- .key = get_cost(needle, candidate->s,
shared->ignore_case),
- };
+ struct Candidate x = batch->xs[i];
+ if (!is_match(needle.b, x.s.b, shared->ignore_case))
continue;
+ x.key = calc_cost(needle, x.s, shared->ignore_case);
+ batch->xs[n++] = x;
}
- batch->len = num_matches;
-
- // Try to fetch a new batch
- if (pthread_mutex_lock(&shared->mutex)) return &job_failed;
- batch = shared->batches < shared->batches_end ?
shared->batches++ : NULL;
- pthread_mutex_unlock(&shared->mutex);
- } while (batch);
+ batch->len = n;
+ }
- return &job_finished;
+ return NULL;
}
-/** Module userdata that gets allocated once at initialization. */
+/** Module userdata allocated at initialization. */
struct Data {
unsigned max_workers;
- struct Worker *workers;
+ pthread_t threads[];
};
-emacs_value hotfuzz_filter(emacs_env *env, ptrdiff_t nargs, emacs_value
args[], void *data_ptr) {
+static emacs_value hotfuzz_filter(emacs_env *env, ptrdiff_t nargs, emacs_value
args[], void *data_ptr) {
struct Data *data = data_ptr;
- emacs_value fcar = env->intern(env, "car"),
- fcdr = env->intern(env, "cdr"),
- fcons = env->intern(env, "cons"),
- nil = env->intern(env, "nil");
+ emacs_value fcar = env->intern(env, "car"), fcdr = env->intern(env,
"cdr"),
+ fcons = env->intern(env, "cons"), nil = env->intern(env, "nil");
struct Bump *bump = NULL;
int success = false;
emacs_value result = nil;
// Collect all candidates
- emacs_value list = args[1];
struct Batch *batches = NULL;
- size_t batch_idx = 0, capacity = 0;
- while (env->is_not_nil(env, list)) {
- if ((batches && batches[batch_idx].len >= BATCH_SIZE ?
++batch_idx : batch_idx)
- >= capacity) {
- capacity = capacity ? 2 * capacity : 1;
+ size_t batch_idx = 0, capacity;
+ for (emacs_value list = args[1]; env->is_not_nil(env, list);
+ list = env->funcall(env, fcdr, 1, (emacs_value[]) {
list })) {
+ if (!batches || (batches[batch_idx].len >= BATCH_SIZE &&
++batch_idx >= capacity)) {
+ capacity = batches ? 2 * capacity : 1;
struct Batch *new_batches;
- if (!(new_batches = realloc(batches, capacity * sizeof
*batches)))
- goto error;
+ if (!(new_batches = realloc(batches, capacity * sizeof
*batches))) goto err;
batches = new_batches;
- for (size_t i = batch_idx; i < capacity; ++i)
- batches[i].len = 0;
+ for (size_t i = batch_idx; i < capacity; ++i)
batches[i].len = 0;
}
- emacs_value value = env->funcall(env, fcar, 1, (emacs_value[])
{list});
- struct Batch *b = batches + batch_idx;
- if (!(b->xs[b->len++].s = copy_emacs_string(env, &bump, value)))
- goto error;
- list = env->funcall(env, fcdr, 1, (emacs_value[]) {list});
+ emacs_value value = env->funcall(env, fcar, 1, (emacs_value[])
{ list });
+ struct Batch *batch = batches + batch_idx;
+ struct Candidate *x = batch->xs + batch->len++;
+ if (!(x->s = copy_emacs_string(env, &bump, x->value =
value)).b) goto err;
}
if (!batches) return nil;
- bool ignore_case = nargs >= 3 && env->is_not_nil(env, args[2]);
- struct EmacsStr *needle = copy_emacs_string(env, &bump, args[0]);
- if (!needle) goto error;
- if (ignore_case)
- for (size_t i = 0; i < needle->len; ++i)
- needle->b[i] = tolower_utf8(needle->b[i]);
+ bool ignore_case = env->is_not_nil(env, args[2]);
+ struct Str needle = copy_emacs_string(env, &bump, args[0]);
+ if (!needle.b) goto err;
+ if (ignore_case) strtolower(needle);
struct Shared shared = {
.ignore_case = ignore_case,
.needle = needle,
.batches = batches,
- .batches_end = batches + batch_idx + 1,
+ .remaining = batch_idx + 1,
};
- if (pthread_mutex_init(&shared.mutex, NULL)) goto error;
- if (pthread_mutex_lock(&shared.mutex)) goto mutex_error;
- enum JobRetVal res = job_finished;
- unsigned worker_count;
-
- struct Worker *workers = data->workers;
- for (worker_count = 0; worker_count < data->max_workers
- && shared.batches < shared.batches_end;
++worker_count) {
- struct Worker *worker = workers + worker_count;
- *worker = (struct Worker) {
- .shared = &shared,
- .batch = shared.batches++,
- };
-
- if (pthread_create(&worker->thread, NULL, worker_routine,
worker)) {
- // Join all workers in order to at least safely destroy
mutex
- res = job_failed;
- break;
- }
- }
- pthread_mutex_unlock(&shared.mutex);
+ unsigned num_workers = 0;
+ for (; num_workers < MIN(data->max_workers, batch_idx + 1);
++num_workers)
+ if (pthread_create(data->threads + num_workers, NULL,
worker_routine, &shared))
+ // Join all workers in order to at least safely free
memory
+ goto err_join_threads;
+ success = true;
+
+err_join_threads:
// Wait for all worker threads
- for (unsigned i = 0; i < worker_count; ++i) {
- enum JobRetVal *retval;
- pthread_join(workers[i].thread, (void **) &retval);
- res |= *retval;
- }
- if (res != job_finished) goto mutex_error;
+ for (unsigned i = 0; i < num_workers; ++i)
pthread_join(data->threads[i], NULL);
+ if (!success) goto err;
- success = true;
- if (env->process_input(env) == emacs_process_input_quit) goto
mutex_error;
+ if (env->process_input(env) == emacs_process_input_quit) goto err;
// Compact all batches
size_t len = batches[0].len;
struct Candidate *xs = batches[0].xs;
- for (struct Batch *b = batches + 1; b < shared.batches_end; ++b) {
+ for (struct Batch *b = batches + 1; b <= batches + batch_idx; ++b) {
unsigned n = b->len;
memmove(xs + len, b->xs, n * sizeof *b->xs);
len += n;
@@ -348,11 +261,9 @@ emacs_value hotfuzz_filter(emacs_env *env, ptrdiff_t
nargs, emacs_value args[],
qsort(xs, len, sizeof *xs, cmp_candidate); // Sort the completions
for (size_t i = len; i-- > 0;)
- result = env->funcall(env, fcons, 2, (emacs_value[])
{xs[i].s->value, result});
+ result = env->funcall(env, fcons, 2, (emacs_value[]) {
xs[i].value, result });
-mutex_error:
- pthread_mutex_destroy(&shared.mutex);
-error:
+err:
free(batches);
bump_free(bump);
@@ -362,27 +273,24 @@ error:
}
int emacs_module_init(struct emacs_runtime *rt) {
- // Verify compatability with Emacs executable loading this module
- if ((size_t) rt->size < sizeof *rt)
- return 1;
+ // Verify compatibility with the Emacs executable loading this module
+ if ((size_t) rt->size < sizeof *rt) return 1;
emacs_env *env = rt->get_environment(rt);
- if ((size_t) env->size < sizeof *env)
- return 2;
+ if ((size_t) env->size < sizeof *env) return 2;
- static struct Data data;
- data.max_workers = sysconf(_SC_NPROCESSORS_ONLN);
- if (!(data.workers = malloc(data.max_workers * sizeof *data.workers)))
- return 1;
+ long max_workers = sysconf(_SC_NPROCESSORS_ONLN);
+ struct Data *data;
+ if (!(data = malloc(max_workers * sizeof *data->threads))) return 1;
+ *data = (struct Data) { max_workers };
env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]) {
env->intern(env, "hotfuzz--filter-c"),
- env->make_function(env, 2, 3, hotfuzz_filter,
+ env->make_function(env, 3, 3, hotfuzz_filter,
"Filter and sort CANDIDATES that match
STRING.\n"
"\n"
- "\(fn STRING CANDIDATES &optional IGNORE-CASE)",
- &data),
+ "\(fn STRING CANDIDATES IGNORE-CASE)",
+ data),
});
-
env->funcall(env, env->intern(env, "provide"), 1,
(emacs_value[]) { env->intern(env, "hotfuzz-module") });
diff --git a/hotfuzz.el b/hotfuzz.el
index bfa39c6de4..6a9d7f79d2 100644
--- a/hotfuzz.el
+++ b/hotfuzz.el
@@ -20,26 +20,23 @@
;;; Code:
-;; See: Myers, Eugene W., and Webb Miller. "Optimal alignments in
-;; linear space." Bioinformatics 4.1 (1988): 11-17.
+;; See: GOTOH, Osamu. An improved algorithm for matching biological
+;; sequences. Journal of molecular biology, 1982, 162.3: 705-708.
(eval-when-compile (require 'cl-lib))
(require 'hotfuzz-module nil t)
(declare-function hotfuzz--filter-c "hotfuzz-module")
-(defgroup hotfuzz nil
- "Fuzzy completion style."
- :group 'minibuffer)
+(defgroup hotfuzz nil "Fuzzy completion style." :group 'minibuffer)
(defcustom hotfuzz-max-highlighted-completions 25
"The number of top-ranking completions that should be highlighted.
Large values will decrease performance."
:type 'integer)
-;; Since the vectors are pre-allocated the optimization where
-;; symmetricity w.r.t. to insertions/deletions means it suffices to
-;; allocate min(#needle, #haystack) for C/D when only calculating the
-;; cost does not apply.
+;; Pre-allocated vectors make the cost-only calulation optimization
+;; where symmetricity w.r.t. insertions/deletions means it suffices to
+;; allocate min(#needle, #haystack) for C/D inapplicable.
(defconst hotfuzz--max-needle-len 128)
(defconst hotfuzz--max-haystack-len 512)
(defvar hotfuzz--c (make-vector hotfuzz--max-needle-len 0))
@@ -101,14 +98,12 @@ and ND/PD respectively may alias."
(aref c (1- m)))))) ; Final cost
(defun hotfuzz-highlight (needle haystack)
- "Highlight the characters that NEEDLE matched in HAYSTACK.
+ "Highlight destructively the characters NEEDLE matched in HAYSTACK.
HAYSTACK has to be a match according to `hotfuzz-all-completions'."
(let ((n (length haystack)) (m (length needle))
- (c hotfuzz--c) (d hotfuzz--d)
+ (c (fillarray hotfuzz--c 10000)) (d (fillarray hotfuzz--d 10000))
(case-fold-search completion-ignore-case))
(unless (or (> n hotfuzz--max-haystack-len) (> m hotfuzz--max-needle-len))
- (fillarray c 10000)
- (fillarray d 10000)
(hotfuzz--calc-bonus haystack)
(cl-loop
with rows initially
@@ -129,12 +124,11 @@ HAYSTACK has to be a match according to
`hotfuzz-all-completions'."
(defun hotfuzz-all-completions (string table &optional pred point)
"Get hotfuzz-completions of STRING in TABLE.
See `completion-all-completions' for the semantics of PRED and POINT.
-This function prematurely sorts the completions; mutating the returned
-list before passing it to `display-sort-function' or
-`cycle-sort-function' will lead to inaccuracies."
- (unless point (setq point (length string)))
+This function prematurely sorts the completions; mutating the result
+before passing it to `display-sort-function' or `cycle-sort-function'
+will lead to inaccuracies."
(let* ((beforepoint (substring string 0 point))
- (afterpoint (substring string point))
+ (afterpoint (if point (substring string point) ""))
(bounds (completion-boundaries beforepoint table pred afterpoint))
(prefix (substring beforepoint 0 (car bounds)))
(needle (substring beforepoint (car bounds)))
@@ -187,11 +181,11 @@ list before passing it to `display-sort-function' or
;;;###autoload
(progn
- ;; Why is the Emacs completions API so cursed?
- (put 'hotfuzz 'completion--adjust-metadata #'hotfuzz--adjust-metadata)
(add-to-list 'completion-styles-alist
'(hotfuzz completion-flex-try-completion hotfuzz-all-completions
- "Fuzzy completion.")))
+ "Fuzzy completion."))
+ ;; Why is the Emacs completions API so cursed?
+ (put 'hotfuzz 'completion--adjust-metadata #'hotfuzz--adjust-metadata))
(provide 'hotfuzz)
;;; hotfuzz.el ends here