fzy

terminal fuzzy finder picker

git clone https://9o.is/git/fzy.git

choices.c

(9079B)


      1 #include <stdlib.h>
      2 #include <stdio.h>
      3 #include <string.h>
      4 #include <pthread.h>
      5 #include <unistd.h>
      6 #include <errno.h>
      7 
      8 #include "options.h"
      9 #include "choices.h"
     10 #include "match.h"
     11 
     12 /* Initial size of buffer for storing input in memory */
     13 #define INITIAL_BUFFER_CAPACITY 4096
     14 
     15 /* Initial size of choices array */
     16 #define INITIAL_CHOICE_CAPACITY 128
     17 
     18 static int cmpchoice(const void *_idx1, const void *_idx2) {
     19 	const struct scored_result *a = _idx1;
     20 	const struct scored_result *b = _idx2;
     21 
     22 	if (a->score == b->score) {
     23 		/* To ensure a stable sort, we must also sort by the string
     24 		 * pointers. We can do this since we know all the strings are
     25 		 * from a contiguous memory segment (buffer in choices_t).
     26 		 */
     27 		if (a->str < b->str) {
     28 			return -1;
     29 		} else {
     30 			return 1;
     31 		}
     32 	} else if (a->score < b->score) {
     33 		return 1;
     34 	} else {
     35 		return -1;
     36 	}
     37 }
     38 
     39 static void *safe_realloc(void *buffer, size_t size) {
     40 	buffer = realloc(buffer, size);
     41 	if (!buffer) {
     42 		fprintf(stderr, "Error: Can't allocate memory (%zu bytes)\n", size);
     43 		abort();
     44 	}
     45 
     46 	return buffer;
     47 }
     48 
     49 static void multiselection_init(choices_t *c) {
     50     c->multiselection_size = 0;
     51     c->multiselections = (unsigned char *) malloc(c->size * sizeof(unsigned char));
     52     for (size_t i = 0; i < c->size; i++) {
     53         c->multiselections[i] = 0;
     54     }
     55 }
     56 
     57 void choices_fread(choices_t *c, FILE *file, char input_delimiter) {
     58 	/* Save current position for parsing later */
     59 	size_t buffer_start = c->buffer_size;
     60 
     61 	/* Resize buffer to at least one byte more capacity than our current
     62 	 * size. This uses a power of two of INITIAL_BUFFER_CAPACITY.
     63 	 * This must work even when c->buffer is NULL and c->buffer_size is 0
     64 	 */
     65 	size_t capacity = INITIAL_BUFFER_CAPACITY;
     66 	while (capacity <= c->buffer_size)
     67 		capacity *= 2;
     68 	c->buffer = safe_realloc(c->buffer, capacity);
     69 
     70 	/* Continue reading until we get a "short" read, indicating EOF */
     71 	while ((c->buffer_size += fread(c->buffer + c->buffer_size, 1, capacity - c->buffer_size, file)) == capacity) {
     72 		capacity *= 2;
     73 		c->buffer = safe_realloc(c->buffer, capacity);
     74 	}
     75 	c->buffer = safe_realloc(c->buffer, c->buffer_size + 1);
     76 	c->buffer[c->buffer_size++] = '\0';
     77 
     78 	/* Truncate buffer to used size, (maybe) freeing some memory for
     79 	 * future allocations.
     80 	 */
     81 
     82 	/* Tokenize input and add to choices */
     83 	const char *line_end = c->buffer + c->buffer_size;
     84 	char *line = c->buffer + buffer_start;
     85 	do {
     86 		char *nl = strchr(line, input_delimiter);
     87 		if (nl)
     88 			*nl++ = '\0';
     89 
     90 		/* Skip empty lines */
     91 		if (*line)
     92 			choices_add(c, line);
     93 
     94 		line = nl;
     95 	} while (line && line < line_end);
     96 
     97 	multiselection_init(c);
     98 }
     99 
    100 static void choices_resize(choices_t *c, size_t new_capacity) {
    101 	c->strings = safe_realloc(c->strings, new_capacity * sizeof(const char *));
    102 	c->capacity = new_capacity;
    103 }
    104 
    105 static void choices_reset_search(choices_t *c) {
    106 	free(c->results);
    107 	c->selection = c->available = 0;
    108 	c->results = NULL;
    109 }
    110 
    111 void choices_init(choices_t *c, options_t *options) {
    112 	c->strings = NULL;
    113 	c->results = NULL;
    114 
    115 	c->buffer_size = 0;
    116 	c->buffer = NULL;
    117 
    118 	c->capacity = c->size = 0;
    119 	choices_resize(c, INITIAL_CHOICE_CAPACITY);
    120 
    121 	if (options->workers) {
    122 		c->worker_count = options->workers;
    123 	} else {
    124 		c->worker_count = (int)sysconf(_SC_NPROCESSORS_ONLN);
    125 	}
    126 
    127 	choices_reset_search(c);
    128 }
    129 
    130 void choices_destroy(choices_t *c) {
    131 	free(c->buffer);
    132 	c->buffer = NULL;
    133 	c->buffer_size = 0;
    134 
    135 	free(c->strings);
    136 	c->strings = NULL;
    137 	c->capacity = c->size = 0;
    138 
    139 	free(c->results);
    140     free(c->multiselections);
    141 	c->results = NULL;
    142     c->multiselections = NULL;
    143 	c->available = c->selection = c->multiselection_size = 0;
    144 }
    145 
    146 void choices_add(choices_t *c, const char *choice) {
    147 	/* Previous search is now invalid */
    148 	choices_reset_search(c);
    149 
    150 	if (c->size == c->capacity) {
    151 		choices_resize(c, c->capacity * 2);
    152 	}
    153 	c->strings[c->size++] = choice;
    154 }
    155 
    156 size_t choices_available(choices_t *c) {
    157 	return c->available;
    158 }
    159 
    160 #define BATCH_SIZE 512
    161 
    162 struct result_list {
    163 	struct scored_result *list;
    164 	size_t size;
    165 };
    166 
    167 struct search_job {
    168 	pthread_mutex_t lock;
    169 	choices_t *choices;
    170 	const char *search;
    171 	size_t processed;
    172 	struct worker *workers;
    173 };
    174 
    175 struct worker {
    176 	pthread_t thread_id;
    177 	struct search_job *job;
    178 	unsigned int worker_num;
    179 	struct result_list result;
    180 };
    181 
    182 static void worker_get_next_batch(struct search_job *job, size_t *start, size_t *end) {
    183 	pthread_mutex_lock(&job->lock);
    184 
    185 	*start = job->processed;
    186 
    187 	job->processed += BATCH_SIZE;
    188 	if (job->processed > job->choices->size) {
    189 		job->processed = job->choices->size;
    190 	}
    191 
    192 	*end = job->processed;
    193 
    194 	pthread_mutex_unlock(&job->lock);
    195 }
    196 
    197 static struct result_list merge2(struct result_list list1, struct result_list list2) {
    198 	size_t result_index = 0, index1 = 0, index2 = 0;
    199 
    200 	struct result_list result;
    201 	result.size = list1.size + list2.size;
    202 	result.list = malloc(result.size * sizeof(struct scored_result));
    203 	if (!result.list) {
    204 		fprintf(stderr, "Error: Can't allocate memory\n");
    205 		abort();
    206 	}
    207 
    208 	while(index1 < list1.size && index2 < list2.size) {
    209 		if (cmpchoice(&list1.list[index1], &list2.list[index2]) < 0) {
    210 			result.list[result_index++] = list1.list[index1++];
    211 		} else {
    212 			result.list[result_index++] = list2.list[index2++];
    213 		}
    214 	}
    215 
    216 	while(index1 < list1.size) {
    217 		result.list[result_index++] = list1.list[index1++];
    218 	}
    219 	while(index2 < list2.size) {
    220 		result.list[result_index++] = list2.list[index2++];
    221 	}
    222 
    223 	free(list1.list);
    224 	free(list2.list);
    225 
    226 	return result;
    227 }
    228 
    229 static void *choices_search_worker(void *data) {
    230 	struct worker *w = (struct worker *)data;
    231 	struct search_job *job = w->job;
    232 	const choices_t *c = job->choices;
    233 	struct result_list *result = &w->result;
    234 
    235 	size_t start, end;
    236 
    237 	for(;;) {
    238 		worker_get_next_batch(job, &start, &end);
    239 
    240 		if(start == end) {
    241 			break;
    242 		}
    243 
    244 		for(size_t i = start; i < end; i++) {
    245 			if (has_match(job->search, c->strings[i])) {
    246 				result->list[result->size].index = i;
    247 				result->list[result->size].str = c->strings[i];
    248 				result->list[result->size].score = match(job->search, c->strings[i]);
    249 				result->size++;
    250 			}
    251 		}
    252 	}
    253 
    254 	/* Sort the partial result */
    255 	qsort(result->list, result->size, sizeof(struct scored_result), cmpchoice);
    256 
    257 	/* Fan-in, merging results */
    258 	for(unsigned int step = 0;; step++) {
    259 		if (w->worker_num % (2 << step))
    260 			break;
    261 
    262 		unsigned int next_worker = w->worker_num | (1 << step);
    263 		if (next_worker >= c->worker_count)
    264 			break;
    265 
    266 		if ((errno = pthread_join(job->workers[next_worker].thread_id, NULL))) {
    267 			perror("pthread_join");
    268 			exit(EXIT_FAILURE);
    269 		}
    270 
    271 		w->result = merge2(w->result, job->workers[next_worker].result);
    272 	}
    273 
    274 	return NULL;
    275 }
    276 
    277 void choices_search(choices_t *c, const char *search) {
    278 	choices_reset_search(c);
    279 
    280 	struct search_job *job = calloc(1, sizeof(struct search_job));
    281 	if (!job) {
    282 		fprintf(stderr, "Error: Can't allocate memory\n");
    283 		abort();
    284 	}
    285 	job->search = search;
    286 	job->choices = c;
    287 	if (pthread_mutex_init(&job->lock, NULL) != 0) {
    288 		fprintf(stderr, "Error: pthread_mutex_init failed\n");
    289 		abort();
    290 	}
    291 	job->workers = calloc(c->worker_count, sizeof(struct worker));
    292 	if (!job->workers) {
    293 		fprintf(stderr, "Error: Can't allocate memory\n");
    294 		abort();
    295 	}
    296 
    297 	struct worker *workers = job->workers;
    298 	for (int i = c->worker_count - 1; i >= 0; i--) {
    299 		workers[i].job = job;
    300 		workers[i].worker_num = i;
    301 		workers[i].result.size = 0;
    302 		workers[i].result.list = malloc(c->size * sizeof(struct scored_result)); /* FIXME: This is overkill */
    303 		if (!workers[i].result.list) {
    304 			fprintf(stderr, "Error: Can't allocate memory\n");
    305 			abort();
    306 		}
    307 
    308 		/* These must be created last-to-first to avoid a race condition when fanning in */
    309 		if ((errno = pthread_create(&workers[i].thread_id, NULL, &choices_search_worker, &workers[i]))) {
    310 			perror("pthread_create");
    311 			exit(EXIT_FAILURE);
    312 		}
    313 	}
    314 
    315 	if (pthread_join(workers[0].thread_id, NULL)) {
    316 		perror("pthread_join");
    317 		exit(EXIT_FAILURE);
    318 	}
    319 
    320 	c->results = workers[0].result.list;
    321 	c->available = workers[0].result.size;
    322 
    323 	free(workers);
    324 	pthread_mutex_destroy(&job->lock);
    325 	free(job);
    326 }
    327 
    328 const char *choices_get(choices_t *c, size_t n) {
    329 	if (n < c->available) {
    330 		return c->results[n].str;
    331 	} else {
    332 		return NULL;
    333 	}
    334 }
    335 
    336 const char *choices_getmulti(choices_t *c, size_t n) {
    337 	if (n < c->size) {
    338 		return c->strings[n];
    339 	} else {
    340 		return NULL;
    341 	}
    342 }
    343 
    344 score_t choices_getscore(choices_t *c, size_t n) {
    345 	return c->results[n].score;
    346 }
    347 
    348 void choices_prev(choices_t *c) {
    349 	if (c->available)
    350 		c->selection = (c->selection + c->available - 1) % c->available;
    351 }
    352 
    353 void choices_next(choices_t *c) {
    354 	if (c->available)
    355 		c->selection = (c->selection + 1) % c->available;
    356 }
    357 
    358 void choices_multiselect_toggle(choices_t *c) {
    359     if (!c->available) return;
    360     size_t index = c->results[c->selection].index;
    361 
    362     if (c->multiselections[index]) {
    363         c->multiselections[index] = 0;
    364         c->multiselection_size--;
    365     } else {
    366         c->multiselections[index] = 1;
    367         c->multiselection_size++;
    368     }
    369 }
    370 
    371 size_t choices_selected(choices_t *c, size_t n) {
    372     if (!c->available) return 0;
    373     size_t index = c->results[n].index;
    374 	return c->multiselections[index];
    375 }