#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <caca.h>
#include <gtcaca/editor.h>
#include <gtcaca/main.h>
#define AUTOC_DEFAULT_MAX_H 9
#define AUTOC_MAX_ITEM_W 30
static int _digits(int n) { int d = 1; while (n >= 10) { n /= 10; d++; } return d; }
static int _ci_eq(char a, char b, int ignore_case)
{
if (!ignore_case) return a == b;
return tolower((unsigned char)a) == tolower((unsigned char)b);
}
static int _prefix(gtcaca_editor_widget_t *w, char *buf, int sz)
{
int n = w->current_pos - w->autoc.pos_start;
if (n < 0) n = 0;
return gtcaca_editor_get_text_range(w, w->autoc.pos_start, w->current_pos, buf, sz < n + 1 ? sz : n + 1);
}
static int _item_matches(const char *item, const char *prefix, int plen, int ignore_case)
{
int i;
for (i = 0; i < plen; i++)
if (!item[i] || !_ci_eq(item[i], prefix[i], ignore_case)) return 0;
return 1;
}
static void _scroll_to_sel(gtcaca_editor_autoc_t *a)
{
if (a->sel < a->top) a->top = a->sel;
if (a->sel >= a->top + a->max_height) a->top = a->sel - a->max_height + 1;
if (a->top < 0) a->top = 0;
}
static void _refilter(gtcaca_editor_widget_t *w)
{
gtcaca_editor_autoc_t *a = &w->autoc;
char prefix[256];
int plen = _prefix(w, prefix, sizeof prefix);
int i;
a->n_filtered = 0;
for (i = 0; i < a->n_items; i++)
if (_item_matches(a->items[i], prefix, plen, a->ignore_case))
a->filtered[a->n_filtered++] = i;
if (a->sel >= a->n_filtered) a->sel = a->n_filtered > 0 ? a->n_filtered - 1 : 0;
if (a->sel < 0) a->sel = 0;
_scroll_to_sel(a);
}
void gtcaca_editor_autoc_cancel(gtcaca_editor_widget_t *w)
{
gtcaca_editor_autoc_t *a = &w->autoc;
int i;
for (i = 0; i < a->n_items; i++) free(a->items[i]);
free(a->items);
free(a->filtered);
a->items = NULL;
a->filtered = NULL;
a->n_items = a->n_filtered = a->sel = a->top = 0;
a->active = 0;
}
void gtcaca_editor_autoc_show(gtcaca_editor_widget_t *w, int len_entered, const char *item_list)
{
gtcaca_editor_autoc_t *a = &w->autoc;
const char *p, *start;
int cap = 0;
gtcaca_editor_autoc_cancel(w);
if (!item_list) return;
a->pos_start = w->current_pos - (len_entered > 0 ? len_entered : 0);
if (a->pos_start < 0) a->pos_start = 0;
p = start = item_list;
for (;;) {
if (*p == a->separator || *p == '\0') {
int len = (int)(p - start);
if (len > 0) {
if (a->n_items == cap) {
cap = cap ? cap * 2 : 16;
char **ni = realloc(a->items, (size_t)cap * sizeof(char *));
int *nf = realloc(a->filtered, (size_t)cap * sizeof(int));
if (!ni || !nf) { free(ni); free(nf); gtcaca_editor_autoc_cancel(w); return; }
a->items = ni; a->filtered = nf;
}
a->items[a->n_items] = malloc((size_t)len + 1);
if (a->items[a->n_items]) { memcpy(a->items[a->n_items], start, (size_t)len); a->items[a->n_items][len] = '\0'; a->n_items++; }
}
if (*p == '\0') break;
start = p + 1;
}
p++;
}
a->active = 1;
a->sel = a->top = 0;
_refilter(w);
if (a->auto_hide && a->n_filtered == 0) gtcaca_editor_autoc_cancel(w);
}
int gtcaca_editor_autoc_active(gtcaca_editor_widget_t *w) { return w->autoc.active; }
int gtcaca_editor_autoc_pos_start(gtcaca_editor_widget_t *w) { return w->autoc.pos_start; }
int gtcaca_editor_autoc_get_current(gtcaca_editor_widget_t *w)
{
gtcaca_editor_autoc_t *a = &w->autoc;
return (a->active && a->n_filtered > 0) ? a->filtered[a->sel] : -1;
}
int gtcaca_editor_autoc_get_current_text(gtcaca_editor_widget_t *w, char *buf, int len)
{
int idx = gtcaca_editor_autoc_get_current(w);
if (idx < 0 || len <= 0) { if (len > 0) buf[0] = '\0'; return 0; }
strncpy(buf, w->autoc.items[idx], (size_t)len - 1);
buf[len - 1] = '\0';
return (int)strlen(buf);
}
void gtcaca_editor_autoc_select(gtcaca_editor_widget_t *w, const char *prefix)
{
gtcaca_editor_autoc_t *a = &w->autoc;
int plen = prefix ? (int)strlen(prefix) : 0, i;
for (i = 0; i < a->n_filtered; i++) {
if (_item_matches(a->items[a->filtered[i]], prefix, plen, a->ignore_case)) { a->sel = i; _scroll_to_sel(a); return; }
}
}
void gtcaca_editor_autoc_complete(gtcaca_editor_widget_t *w)
{
gtcaca_editor_autoc_t *a = &w->autoc;
char chosen[256];
int start;
if (!a->active) return;
if (a->n_filtered == 0) { gtcaca_editor_autoc_cancel(w); return; }
strncpy(chosen, a->items[a->filtered[a->sel]], sizeof chosen - 1);
chosen[sizeof chosen - 1] = '\0';
start = a->pos_start;
if (w->current_pos > start) gtcaca_editor_delete_range(w, start, w->current_pos - start);
gtcaca_editor_insert_text(w, start, chosen);
gtcaca_editor_set_empty_selection(w, start + (int)strlen(chosen));
gtcaca_editor_autoc_cancel(w);
if (w->autoc_cb) w->autoc_cb(w, chosen, w->autoc_cb_userdata);
}
void gtcaca_editor_autoc_set_separator(gtcaca_editor_widget_t *w, char sep) { w->autoc.separator = sep ? sep : ' '; }
char gtcaca_editor_autoc_get_separator(gtcaca_editor_widget_t *w) { return w->autoc.separator; }
void gtcaca_editor_autoc_set_ignore_case(gtcaca_editor_widget_t *w, int on) { w->autoc.ignore_case = on ? 1 : 0; }
void gtcaca_editor_autoc_set_auto_hide(gtcaca_editor_widget_t *w, int on) { w->autoc.auto_hide = on ? 1 : 0; }
void gtcaca_editor_autoc_set_cancel_at_start(gtcaca_editor_widget_t *w, int on) { w->autoc.cancel_at_start = on ? 1 : 0; }
void gtcaca_editor_autoc_set_max_height(gtcaca_editor_widget_t *w, int rows) { w->autoc.max_height = rows > 0 ? rows : 1; }
void gtcaca_editor_autoc_set_fillups(gtcaca_editor_widget_t *w, const char *chars) { free(w->autoc.fillups); w->autoc.fillups = chars ? strdup(chars) : NULL; }
void gtcaca_editor_autoc_stops(gtcaca_editor_widget_t *w, const char *chars) { free(w->autoc.stops); w->autoc.stops = chars ? strdup(chars) : NULL; }
void gtcaca_editor_set_autoc_cb(gtcaca_editor_widget_t *w, gtcaca_editor_autoc_cb_t cb, void *userdata) { w->autoc_cb = cb; w->autoc_cb_userdata = userdata; }
int _gtcaca_editor_autoc_key(gtcaca_editor_widget_t *w, int key)
{
gtcaca_editor_autoc_t *a = &w->autoc;
if (!a->active) return 0;
switch (key) {
case CACA_KEY_UP:
case CACA_KEY_CTRL_P:
if (a->sel > 0) a->sel--;
_scroll_to_sel(a);
return 1;
case CACA_KEY_DOWN:
case CACA_KEY_CTRL_N:
if (a->sel < a->n_filtered - 1) a->sel++;
_scroll_to_sel(a);
return 1;
case CACA_KEY_PAGEUP:
a->sel -= a->max_height; if (a->sel < 0) a->sel = 0; _scroll_to_sel(a);
return 1;
case CACA_KEY_PAGEDOWN:
a->sel += a->max_height; if (a->sel >= a->n_filtered) a->sel = a->n_filtered - 1; if (a->sel < 0) a->sel = 0; _scroll_to_sel(a);
return 1;
case CACA_KEY_RETURN:
case 10:
case CACA_KEY_TAB:
gtcaca_editor_autoc_complete(w);
return 1;
case CACA_KEY_ESCAPE:
case CACA_KEY_CTRL_G:
gtcaca_editor_autoc_cancel(w);
return 1;
case CACA_KEY_BACKSPACE:
case CACA_KEY_DELETE:
if (w->current_pos <= a->pos_start) {
if (a->cancel_at_start) gtcaca_editor_autoc_cancel(w);
return 0;
}
gtcaca_editor_delete_back(w);
_refilter(w);
if (a->auto_hide && a->n_filtered == 0) gtcaca_editor_autoc_cancel(w);
return 1;
case CACA_KEY_LEFT:
case CACA_KEY_RIGHT:
case CACA_KEY_HOME:
case CACA_KEY_END:
gtcaca_editor_autoc_cancel(w);
return 0;
default:
if (key >= 32 && key <= 126) {
char c = (char)key;
if (a->stops && strchr(a->stops, c)) { gtcaca_editor_autoc_cancel(w); return 0; }
if (a->fillups && strchr(a->fillups, c)) { gtcaca_editor_autoc_complete(w); return 0; }
if (isalnum((unsigned char)c) || c == '_' || c == '$') {
gtcaca_editor_add_char(w, c);
_refilter(w);
if (a->auto_hide && a->n_filtered == 0) gtcaca_editor_autoc_cancel(w);
return 1;
}
gtcaca_editor_autoc_cancel(w);
return 0;
}
return 0;
}
}
void _gtcaca_editor_draw_autoc(gtcaca_editor_widget_t *w)
{
gtcaca_editor_autoc_t *a = &w->autoc;
int line_count = gtcaca_editor_get_line_count(w);
int text_h = w->height - 2;
int total_w = w->width - 2;
int margin_w = w->show_line_numbers ? _digits(line_count) + 1 : 0;
int text_w = total_w - margin_w;
int inner_x = w->x + 1, inner_y = w->y + 1, origin_x = inner_x + margin_w;
int caret_line = gtcaca_editor_line_from_position(w, w->current_pos);
int word_col = gtcaca_editor_get_column(w, a->pos_start);
int nshow, popw, pop_x, pop_y, r, i, maxlen = 0;
if (!a->active || a->n_filtered == 0 || text_h <= 0 || text_w <= 0) return;
nshow = a->n_filtered < a->max_height ? a->n_filtered : a->max_height;
for (i = 0; i < a->n_filtered; i++) {
int l = (int)strlen(a->items[a->filtered[i]]);
if (l > maxlen) maxlen = l;
}
popw = maxlen; if (popw > AUTOC_MAX_ITEM_W) popw = AUTOC_MAX_ITEM_W; if (popw < 6) popw = 6;
if (popw > text_w) popw = text_w;
pop_x = origin_x + (word_col - w->x_offset);
if (pop_x + popw > inner_x + text_w) pop_x = inner_x + text_w - popw;
if (pop_x < inner_x) pop_x = inner_x;
pop_y = inner_y + (caret_line - w->first_visible_line) + 1;
if (pop_y + nshow > inner_y + text_h)
pop_y = inner_y + (caret_line - w->first_visible_line) - nshow;
if (pop_y < inner_y) pop_y = inner_y;
for (r = 0; r < nshow; r++) {
int idx = a->top + r;
int col;
const char *item;
int selected;
if (idx >= a->n_filtered) break;
item = a->items[a->filtered[idx]];
selected = (idx == a->sel);
if (selected) caca_set_color_ansi(gmo.cv, CACA_WHITE, CACA_BLUE);
else caca_set_color_ansi(gmo.cv, CACA_BLACK, CACA_LIGHTGRAY);
for (col = 0; col < popw; col++) {
char ch = (col < (int)strlen(item)) ? item[col] : ' ';
caca_put_char(gmo.cv, pop_x + col, pop_y + r, ch);
}
if (a->n_filtered > nshow) {
int thumb = (a->sel * (nshow - 1)) / (a->n_filtered - 1);
caca_set_color_ansi(gmo.cv, CACA_WHITE, CACA_DARKGRAY);
caca_put_char(gmo.cv, pop_x + popw - 1, pop_y + r, r == thumb ? '#' : ' ');
}
}
}