["\'`])|(?\[)|(?\())'; + + $matcher = generate_matcher('.*?'); + + # Here we match text till enclosing pair, using perl conditionals in + # regexps (?(condition)yes-expression|no-expression). + # \0 is used to hack concatenation with '*' later in the code. + $char_class_at_end = '.*?(.(?=(?()\]|((?(
)\)|\g{q})))))\0'; + $char_class_to_complete = '\S'; +} + + +# use the last used word or read the word behind the cursor +my $word_to_complete = read_word_at_coord($self, $cursor_row, $cursor_column, + $char_class_to_complete); + +print stdout "$word_to_complete\n"; + +if ($word_to_complete) { + while (1) { + # ignore the completed word itself + $self->{already_completed}{$word_to_complete} = 1; + + # continue the last search or start from the current row + my $completion = find_match($self, + $word_to_complete, + $self->{next_row} // $cursor_row, + $matcher->($word_to_complete), + $char_class_before, + $char_class_at_end); + if ($completion) { + print stdout $completion."\n".join ("\n", @{$self->{highlight}})."\n"; + } + else { + last; + } + } +} + +###################################################################### + +sub highlight_match { + my ($self, $linenum, $completion) = @_; + + # clear_highlight($self); + + my $line = @{$lines}[$linenum]; + my $re = quotemeta $completion; + + $line =~ /$re/; + + my $beg = $-[0]; + my $end = $+[0]; + + if ($linenum >= $lines_before_cursor) + { + $lline = $last_line - $lines_before_cursor; + $linenum -= $lines_before_cursor; + $linenum = $lline - $linenum; + $linenum += $lines_before_cursor; + } + + + $self->{highlight} = [$linenum, $beg, $end]; +} + +###################################################################### + +sub read_word_at_coord { + my ($self, $row, $col, $char_class) = @_; + + $_ = substr(@{$lines} [$row], 0, $col); # get the current line up to the cursor... + s/.*?($char_class*)$/$1/; # ...and read the last word from it + return $_; +} + +###################################################################### + +# Returns a function that takes a string and returns that string with +# this function's argument inserted between its every two characters. +# The resulting string is used as a regular expression matching the +# completion candidates. +sub generate_matcher { + my $regex_between = shift; + + sub { + $_ = shift; + + # sorry for this lispy code, I couldn't resist ;) + (join "$regex_between", + (map quotemeta, + (split //))) + } +} + +###################################################################### + +# Checks whether the completion found by find_match() was already +# found and if it was, calls find_match() again to find the next +# completion. +# +# Takes all the arguments that find_match() would take, to make a +# mutually recursive call. +sub skip_duplicates { + my ($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end) = @_; + my $completion; + + if ($current_row <= $lines_before_cursor) + { + $completion = shift @{$self->{matches_in_row}}; # get the leftmost one + } + else + { + $completion = pop @{$self->{matches_in_row}}; # get the leftmost one + } + + # check for duplicates + if (exists $self->{already_completed}{$completion}) { + # skip this completion + return find_match(@_); + } else { + $self->{already_completed}{$completion} = 1; + + highlight_match($self, + $self->{next_row}+1, + $completion); + + return $completion; + } +} + +###################################################################### + +# Finds the next matching completion in the row current row or above +# while skipping duplicates using skip_duplicates(). +sub find_match { + my ($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end) = @_; + $self->{matches_in_row} //= []; + + # cycle through all the matches in the current row if not starting a new search + if (@{$self->{matches_in_row}}) { + return skip_duplicates($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end); + } + + + my $i; + # search through all the rows starting with current one or one above the last checked + for ($i = $current_row; $i >= 0; --$i) { + my $line = @{$lines}[$i]; # get the line of text from the row + + # if ($i == $cursor_row) { + # $line = substr $line, 0, $cursor_column; + # } + + $_ = $line; + + # find all the matches in the current line + my $match; + push @{$self->{matches_in_row}}, $+{match} while ($_, $match) = / + (.*${char_class_before}) + (?
+ ${regexp} + ${char_class_at_end}* + ) + /ix; + # corner case: match at the very beginning of line + push @{$self->{matches_in_row}}, $+{match} if $line =~ /^(${char_class_before}){0}(? $regexp$char_class_at_end*)/i; + + if (@{$self->{matches_in_row}}) { + # remember which row should be searched next + $self->{next_row} = --$i; + + # arguments needed for find_match() mutual recursion + return skip_duplicates($self, $word_to_match, $i, $regexp, $char_class_before, $char_class_at_end); + } + } + + # # no more possible completions, revert to the original word + # undo_completion($self) if $i < 0; + + return undef; +} diff --git a/st.c b/st.c index cea84d2..5094f5b 100644 --- a/st.c +++ b/st.c @@ -17,6 +17,7 @@ #include #include +#include "autocomplete.h" #include "st.h" #include "win.h" @@ -2720,6 +2721,8 @@ tresize(int col, int row) return; } + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); + /* Shift buffer to keep the cursor where we expect it */ if (row <= term.c.y) { term.screen[0].cur = (term.screen[0].cur - row + term.c.y + 1) % term.screen[0].size; @@ -2840,3 +2843,227 @@ redraw(void) tfulldirt(); draw(); } + +void autocomplete (const Arg *arg) { + static _Bool active = 0; + int acmpl_cmdindex = arg->i; + static int acmpl_cmdindex_prev; + + if (active == 0) + acmpl_cmdindex_prev = acmpl_cmdindex; + + static const char * const acmpl_cmd[] = { + [ACMPL_DEACTIVATE] = "__DEACTIVATE__", + [ACMPL_WORD] = "word-complete", + [ACMPL_WWORD] = "WORD-complete", + [ACMPL_FUZZY_WORD] = "fuzzy-word-complete", + [ACMPL_FUZZY_WWORD] = "fuzzy-WORD-complete", + [ACMPL_FUZZY] = "fuzzy-complete", + [ACMPL_SUFFIX] = "suffix-complete", + [ACMPL_SURROUND] = "surround-complete", + [ACMPL_UNDO] = "__UNDO__", + }; + + static FILE *acmpl_exec = NULL; + static int acmpl_status; + static char *stbuffile; + static char *target = NULL; + static size_t targetlen; + static char *completion = NULL; + static size_t complen_prev = 0; + static int cx, cy; + + if (acmpl_cmdindex == ACMPL_DEACTIVATE) { + if (active) { + active = 0; + pclose(acmpl_exec); + unlink(stbuffile); + free(stbuffile); + stbuffile = NULL; + + if (complen_prev) { + selclear(); + complen_prev = 0; + } + } + return; + } + + if (acmpl_cmdindex == ACMPL_UNDO) { + if (active) { + active = 0; + pclose(acmpl_exec); + unlink(stbuffile); + free(stbuffile); + stbuffile = NULL; + + if (complen_prev) { + selclear(); + for (size_t i = 0; i < complen_prev; i++) + ttywrite((char[]) {'\b'}, 1, 1); + complen_prev = 0; + ttywrite(target, targetlen, 0); + } + } + return; + } + + if (acmpl_cmdindex != acmpl_cmdindex_prev) { + if (active) { + acmpl_cmdindex_prev = acmpl_cmdindex; + goto acmpl_begin; + } + } + + if (active == 0) { + acmpl_cmdindex_prev = acmpl_cmdindex; + cx = term.c.x; + cy = term.c.y; + + char filename[] = "/tmp/st-autocomplete-XXXXXX"; + int fd = mkstemp(filename); + + if (fd == -1) { + perror("mkstemp"); + return; + } + + stbuffile = strdup(filename); + + FILE *stbuf = fdopen(fd, "w"); + if (!stbuf) { + perror("fdopen"); + close(fd); + unlink(stbuffile); + free(stbuffile); + stbuffile = NULL; + return; + } + + char *stbufline = malloc(term.col + 2); + if (!stbufline) { + perror("malloc"); + fclose(stbuf); + unlink(stbuffile); + free(stbuffile); + stbuffile = NULL; + return; + } + + int cxp = 0; + for (size_t y = 0; y < term.row; y++) { + if (y == term.c.y) cx += cxp * term.col; + + size_t x = 0; + for (; x < term.col; x++) + utf8encode(term.line[y][x].u, stbufline + x); + if (term.line[y][x - 1].mode & ATTR_WRAP) { + x--; + if (y <= term.c.y) cy--; + cxp++; + } else { + stbufline[x] = '\n'; + cxp = 0; + } + stbufline[x + 1] = 0; + fputs(stbufline, stbuf); + } + + free(stbufline); + fclose(stbuf); + +acmpl_begin: + target = malloc(term.col + 1); + completion = malloc(term.col + 1); + if (!target || !completion) { + perror("malloc"); + free(target); + free(completion); + unlink(stbuffile); + free(stbuffile); + stbuffile = NULL; + return; + } + + char acmpl[1500]; + snprintf(acmpl, sizeof(acmpl), + "cat %s | st-autocomplete %s %d %d", + stbuffile, acmpl_cmd[acmpl_cmdindex], cy, cx); + + acmpl_exec = popen(acmpl, "r"); + if (!acmpl_exec) { + perror("popen"); + free(target); + free(completion); + unlink(stbuffile); + free(stbuffile); + stbuffile = NULL; + return; + } + + if (fscanf(acmpl_exec, "%s\n", target) != 1) { + perror("fscanf"); + pclose(acmpl_exec); + free(target); + free(completion); + unlink(stbuffile); + free(stbuffile); + stbuffile = NULL; + return; + } + targetlen = strlen(target); + } + + unsigned line, beg, end; + + acmpl_status = fscanf(acmpl_exec, "%[^\n]\n%u\n%u\n%u\n", completion, &line, &beg, &end); + if (acmpl_status == EOF) { + if (active == 0) { + pclose(acmpl_exec); + free(target); + free(completion); + unlink(stbuffile); + free(stbuffile); + stbuffile = NULL; + return; + } + active = 0; + pclose(acmpl_exec); + ttywrite(target, targetlen, 0); + goto acmpl_begin; + } + + active = 1; + + if (complen_prev == 0) { + for (size_t i = 0; i < targetlen; i++) + ttywrite((char[]) {'\b'}, 1, 1); + } else { + selclear(); + for (size_t i = 0; i < complen_prev; i++) + ttywrite((char[]) {'\b'}, 1, 1); + complen_prev = 0; + } + + complen_prev = strlen(completion); + ttywrite(completion, complen_prev, 0); + + if (line == cy && beg > cx) { + beg += complen_prev - targetlen; + end += complen_prev - targetlen; + } + + end--; + + int wl = 0; + int tl = line; + for (int l = 0; l < tl; l++) + if (term.line[l][term.col - 1].mode & ATTR_WRAP) { + wl++; + tl++; + } + + selstart(beg % term.col, line + wl + beg / term.col, 0); + selextend(end % term.col, line + wl + end / term.col, 1, 0); + xsetsel(getsel()); +} diff --git a/st.h b/st.h index 09ad2bf..06d8f8a 100644 --- a/st.h +++ b/st.h @@ -78,6 +78,8 @@ typedef union { const char *s; } Arg; +void autocomplete (const Arg *); + void die(const char *, ...); void redraw(void); void draw(void); diff --git a/x.c b/x.c index 1958fd2..a612a46 100644 --- a/x.c +++ b/x.c @@ -1867,11 +1867,20 @@ kpress(XEvent *ev) /* 1. shortcuts */ for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) { if (ksym == bp->keysym && match(bp->mod, e->state)) { + if (bp -> func != autocomplete) + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); bp->func(&(bp->arg)); return; } } + if (!( + len == 0 && + e -> state & ~ignoremod // ACMPL_ISSUE: I'm not sure that this is the right way + | ACMPL_MOD == ACMPL_MOD + )) + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); + /* 2. custom keys from config.h */ if ((customkey = kmap(ksym, e->state))) { ttywrite(customkey, strlen(customkey), 1);