Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions USER_DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ escape hatches above get you back to your previous shell at any time.
current directory (also `gmake`, `/usr/bin/make`, and behind `sudo`, `env X=1`,
`&&`, `(`; a wrapper option that takes an argument, as in `sudo -u root make`,
falls back to filenames). The file is parsed in the shell itself; make is never run.
Targets you have run (history lines starting with `make` or `gmake`) come first,
most-run (weighted toward recent runs) first;
the rest follow alphabetically. With `HELLISH_SUGGEST_STRATEGY=recent` the
run ones are ordered by how recently they ran.

- Files read: the first regular file of `GNUmakefile`, `makefile`, `Makefile`
(GNU make's order, symlinks followed), at most 4 MiB. No such file, a
Expand All @@ -194,8 +198,9 @@ falls back to filenames). The file is parsed in the shell itself; make is never
|---|---|
| `HELLISH_BANNER=0\|1` | welcome panel off / on |
| `HELLISH_HISTORY_PREFIX=0\|off` | Up/Down plain history; default is prefix search, read at every prompt |
| `HELLISH_AUTOSUGGEST=1\|on` | inline suggestion, off by default: the newest history line that starts with what you typed, shown dim after the cursor (see below); read at every prompt |
| `HELLISH_AUTOSUGGEST=1\|on` | inline suggestion, off by default: the best-ranked history line (see `HELLISH_SUGGEST_STRATEGY`) that starts with what you typed, shown dim after the cursor (see below); read at every prompt |
| `HELLISH_SUGGEST_MAX=N` | how many of the newest history lines the suggestion looks at (default 1000; not a number, or below 1, is the default); read at every prompt |
| `HELLISH_SUGGEST_STRATEGY=recent` | rank suggestions newest first; by default (unset or any other value) they are ranked by frecency: how often a line was run, weighted toward recent runs. Also orders `make <TAB>`. Read at every prompt |
| `HELLISH_NO_UPDATE_CHECK=1` | never check for updates, no badge |
| `HELLISH_NO_ANIM=1` | no startup animation |
| `HELLISH_NO_EXEC=1` | user-install hook: skip for this one session |
Expand All @@ -219,10 +224,10 @@ typed, and the hint is wiped from the screen.
A word is the blanks at the front of the hint plus the run of characters up
to the next space or tab (so `-m`, `a/b`, `x=1` are one word each). Alt-N
and Alt-P step through distinct lines (same full text offered once), look
at no more than `HELLISH_SUGGEST_MAX` entries, ring the bell at the oldest
and the newest end instead of wrapping, and start over from the newest
at no more than `HELLISH_SUGGEST_MAX` entries, go in ranking order, ring the bell at the last and
the first instead of wrapping, and start over from the best
match as soon as the line is edited. Right after Alt-N inserts the line
shown, not the newest. A hint a zle widget set as `POSTDISPLAY` is not
shown, not the best-ranked one. A hint a zle widget set as `POSTDISPLAY` is not
cycled or taken by words. In vi insert mode Alt-F / Alt-N / Alt-P work the
same (no Ctrl-Right). `unset HELLISH_AUTOSUGGEST`
(or `=0`) turns it off at the next prompt. A key you bound yourself
Expand Down
6 changes: 6 additions & 0 deletions incs/history.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

# include "shell.h"
# include <sys/types.h>
# include <stdint.h>

/* hist_cmds is the truth; readline's own list is a mirror kept in step so
the arrow keys agree with what `history` prints.
Expand Down Expand Up @@ -67,5 +68,10 @@ t_string encode_cmd_hist(char *cmd);
void manage_history(t_shell *state);
bool worthy_of_being_remembered(t_shell *state);
void add_history_line(t_shell *state, const char *cmd);
/* Frecency (history_frecency.c). */
uint32_t hist_weight(size_t age);
uint64_t hist_score_add(uint64_t score, size_t len, size_t idx,
bool recent);
size_t hist_window_floor(size_t len, int max);

#endif
6 changes: 4 additions & 2 deletions incs/prompt.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ typedef struct s_rl
char *sg_post; /* POSTDISPLAY a widget set (zle_rl5.c) */
char *sg_post_at; /* the line it was set for */
char *sg_pub; /* what a widget was handed as POSTDISPLAY */
t_vec sg_cyc; /* history indices offered (size_t) */
size_t sg_cyc_k; /* which of them is on screen */
bool sg_recent; /* HELLISH_SUGGEST_STRATEGY=recent */
t_vec sg_rank; /* ranked history indices (size_t) */
t_vec sg_scr; /* ranking scratch (t_sg_ent), reused */
size_t sg_cyc_k; /* which of sg_rank is on screen */
char *sg_cyc_line; /* line the cycle is for; NULL: none */
bool sg_painted; /* a ghost is on screen right now */
int sg_col; /* column it starts at */
Expand Down
1 change: 1 addition & 0 deletions incs/shell.h
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ typedef struct s_make_cache
long long mtime_s;
long long mtime_ns;
t_vec targets; /* char *, xmalloc'd */
t_vec order; /* const char *, targets ranked for this TAB */
} t_make_cache;

typedef struct s_shell
Expand Down
19 changes: 16 additions & 3 deletions src/completion/complete_make.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
** TAB. LIMITATION: a change to an INCLUDED file alone does not invalidate
** it -- touch the Makefile.
**
** ORDER. Targets used in history come first (frecency, see
** complete_make_rank.c), then the rest alphabetically.
**
** WHEN IT STAYS OUT. A word starting with '-' or holding '=' or '/' is not
** a target; and if an earlier argument is -C, --directory*, -f, --file* or
** --makefile*, the Makefile is not the one in the cwd, so filenames win.
Expand All @@ -54,12 +57,12 @@ static char *mk_generator(const char *text, int state)
t_vec *v;
char *s;

v = &(*zle_state_cell())->make_cache.targets;
v = &(*zle_state_cell())->make_cache.order;
if (!state)
i = 0;
while (i < v->len)
{
s = ((char **)v->ctx)[i++];
s = (char *)((const char **)v->ctx)[i++];
if (!ft_strncmp(s, text, ft_strlen(text)))
return (rl_dup(s));
}
Expand Down Expand Up @@ -90,11 +93,21 @@ static int mk_diverts(t_pv_line *a)
return (0);
}

/* The matches come out of mk_generator already in the order worth
showing (mk_order), so readline must not sort them again: it does that
AFTER this returns, so the flag is cleared here and cmd_completion sets
it back at the start of every attempt (completion.c). */
char **comp_make(t_shell *st, const char *text, t_pv_line *a)
{
char **m;

if (text[0] == '-' || ft_strchr(text, '=') || ft_strchr(text, '/'))
return (NULL);
if (mk_diverts(a) || !mk_load(st))
return (NULL);
return (rl_completion_matches(text, mk_generator));
mk_order(st);
m = rl_completion_matches(text, mk_generator);
if (m)
rl_sort_completion_matches = 0;
return (m);
}
25 changes: 25 additions & 0 deletions src/completion/complete_make.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ typedef struct s_mk_scan
int def;
} t_mk_scan;

/* A target while ranking by history: how much it was used, and where. */
typedef struct s_mk_use
{
const char *name;
uint64_t score;
size_t newest;
size_t seen;
} t_mk_use;

/* What counting needs besides the targets: the history length (for age)
and the strategy. */
typedef struct s_mk_rank
{
t_mk_use *u;
size_t cnt;
size_t len;
bool recent;
} t_mk_rank;

# define MK_MAX 4194304

/* The nanosecond mtime field is st_mtim on POSIX 2008, st_mtimespec on
Expand All @@ -62,11 +81,17 @@ typedef struct s_mk_scan
size_t pc_cmd_off(int start);
size_t prov_word(t_pv_line *p, const char **w);
size_t prov_base(const char *w, size_t n);
bool prov_is_make(const char *w, size_t n);
char **comp_provider_try(const char *text, int start, int end);
char **comp_make(t_shell *st, const char *text, t_pv_line *a);
char *rl_dup(const char *s);
t_vec *mk_load(t_shell *st);
void mk_cache_free(t_shell *st);
void mk_order(t_shell *st);
int mk_cmp_use(const void *x, const void *y);
int mk_cmp_name(const void *x, const void *y);
void mk_count_entry(t_mk_rank *r, const char *e, size_t idx);
bool mk_is_make_entry(const char *e);
char *mk_slurp(const char *path, size_t *len);
void mk_scan(t_mk_scan *m, const char *buf, size_t len);
void mk_line(t_mk_scan *m, char *l);
Expand Down
1 change: 1 addition & 0 deletions src/completion/complete_make_cache.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ void mk_clear(t_make_cache *c)
while (i < c->targets.len)
xfree(((char **)c->targets.ctx)[i++]);
xfree(c->targets.ctx);
xfree(c->order.ctx);
xfree(c->name);
ft_bzero(c, sizeof(*c));
}
Expand Down
130 changes: 130 additions & 0 deletions src/completion/complete_make_rank.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* complete_make_rank.c :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: dlesieur <dlesieur@student.42.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/10/17 12:00:00 by dlesieur #+# #+# */
/* Updated: 2026/10/17 12:00:00 by dlesieur ### ########.fr */
/* */
/* ************************************************************************** */

#include "complete_make.h"
#include <string.h>

/* make <TAB> ordering: the targets the history shows being used come
** first, best frecency first (history_frecency.c, the ranking the inline
** suggestion uses); the rest follow alphabetically.
**
** An entry counts for a target when its FIRST word is a command the
** provider table maps to comp_make (make, gmake, any path to them) and
** the target is one of its whitespace-separated words, once per entry.
**
** Caveat: this reads words, not make's command line. `make -C re test`
** credits a target named `re` for the directory, and in `make a && make b`
** the `b` counts because the entry starts with make: a target can rank
** higher than it deserves, but is never hidden. `sudo make x` is not
** counted, and a word with `;`, a quote or a backslash glued on never
** equals a target name, so those under-credit. HELLISH_SUGGEST_STRATEGY=
** recent, or an alphabetical list, is the way out. */

/* Alphabetical, the way readline sorts when it is allowed to (strcoll),
so an unused-only list looks exactly as it always did. */
int mk_cmp_name(const void *x, const void *y)
{
const t_mk_use *a;
const t_mk_use *b;

a = x;
b = y;
return (strcoll(a->name, b->name));
}

/* Used targets first: best score, then newest; ties, and every unused
target (score 0), by name. */
int mk_cmp_use(const void *x, const void *y)
{
const t_mk_use *a;
const t_mk_use *b;

a = x;
b = y;
if (a->score != b->score && a->score < b->score)
return (1);
if (a->score != b->score)
return (-1);
if (a->newest != b->newest && a->newest < b->newest)
return (1);
if (a->newest != b->newest)
return (-1);
return (mk_cmp_name(x, y));
}

/* Is the entry a make invocation: its first word names the make provider
(the same table row that decides `make <TAB>` itself). */
bool mk_is_make_entry(const char *e)
{
size_t n;
size_t b;

while (*e == ' ' || *e == '\t')
e++;
n = 0;
while (e[n] && e[n] != ' ' && e[n] != '\t')
n++;
b = prov_base(e, n);
return (prov_is_make(e + b, n - b));
}

/* Binary search of the name-sorted `u` for the word w[0..n); NULL if it
is not a target. */
static t_mk_use *mk_find(t_mk_use *u, size_t cnt, const char *w, size_t n)
{
size_t lo;
size_t hi;
size_t mid;
int c;

lo = 0;
hi = cnt;
while (lo < hi)
{
mid = lo + (hi - lo) / 2;
c = ft_strncmp(u[mid].name, w, n);
if (!c && u[mid].name[n])
c = 1;
if (!c)
return (&u[mid]);
if (c < 0)
lo = mid + 1;
else
hi = mid;
}
return (NULL);
}

/* Credit entry `idx` (text `e`) to every distinct target among its words.
`seen` stops a word repeated in one entry from counting twice. */
void mk_count_entry(t_mk_rank *r, const char *e, size_t idx)
{
t_mk_use *t;
size_t n;

while (*e)
{
while (*e == ' ' || *e == '\t')
e++;
n = 0;
while (e[n] && e[n] != ' ' && e[n] != '\t')
n++;
t = mk_find(r->u, r->cnt, e, n);
if (t && n && t->seen != idx + 1)
{
t->seen = idx + 1;
t->score = hist_score_add(t->score, r->len, idx, r->recent);
t->newest = idx;
}
e += n;
}
}
80 changes: 80 additions & 0 deletions src/completion/complete_make_rank2.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* complete_make_rank2.c :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: dlesieur <dlesieur@student.42.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/10/17 12:00:00 by dlesieur #+# #+# */
/* Updated: 2026/10/17 12:00:00 by dlesieur ### ########.fr */
/* */
/* ************************************************************************** */

#include "complete_make.h"
#include <string.h>
#include <stdlib.h>

/* Fill st->make_cache.order with the cached targets in the order
`make <TAB>` offers them (complete_make_rank.c): used first, by
frecency, then the rest alphabetically. */

/* One record per target, sorted by name so that words can be looked up
by binary search. Returns NULL for no targets or no memory. */
static t_mk_use *mk_records(t_vec *targets)
{
t_mk_use *u;
size_t i;

if (!targets->len)
return (NULL);
u = xmalloc(targets->len * sizeof(*u));
i = 0;
while (i < targets->len)
{
u[i] = (t_mk_use){((char **)targets->ctx)[i], 0, 0, 0};
i++;
}
qsort(u, targets->len, sizeof(*u), mk_cmp_name);
return (u);
}

/* Walk the window of the newest HELLISH_SUGGEST_MAX history entries, the
same one the inline suggestion looks at, crediting make invocations.
Cost: O(window * words * log targets) per TAB. */
static void mk_credit(t_shell *st, t_mk_rank *r)
{
char **a;
size_t i;

a = (char **)st->hist.hist_cmds.ctx;
i = hist_window_floor(r->len, st->rl.sg_max);
while (i < r->len)
{
if (a[i] && mk_is_make_entry(a[i]))
mk_count_entry(r, a[i], i);
i++;
}
}

/* The ranked order, as borrowed pointers into the target list. It is
rebuilt on every TAB: the history moves on, the targets rarely do. */
void mk_order(t_shell *st)
{
t_mk_rank r;
size_t i;

st->make_cache.order.len = 0;
st->make_cache.order.elem_size = sizeof(const char *);
r.cnt = st->make_cache.targets.len;
r.u = mk_records(&st->make_cache.targets);
r.len = st->hist.hist_cmds.len;
r.recent = st->rl.sg_recent;
if (!r.u)
return ;
mk_credit(st, &r);
qsort(r.u, r.cnt, sizeof(*r.u), mk_cmp_use);
i = 0;
while (i < r.cnt)
vec_push(&st->make_cache.order, &r.u[i++].name);
xfree(r.u);
}
Loading
Loading