["\'`])|(?\[)|(?\())'; + + $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'; +} + +my $lines = []; + +my $last_line = -1; +my $lines_after_cursor = 0; + +while (
) +{ + $last_line++; + + if ($last_line <= $cursor_row) + { + push @{$lines}, $_; + } + else + { + unshift @{$lines}, $_; + $lines_after_cursor++; + } +} + +$cursor_row = $last_line; + +# read the word behind the cursor +$_ = substr(@{$lines} [$cursor_row], 0, $cursor_column); # get the current line up to the cursor... +s/.*?($char_class_to_complete*)$/$1/; # ...and read the last word from it +my $word_to_complete = quotemeta; + +# ignore the completed word itself +$self->{already_completed}{$word_to_complete} = 1; + +print stdout "$word_to_complete\n"; + +# search for matches +while (my $completion = find_match($self, + $word_to_complete, + $self->{next_row} // $cursor_row, + $matcher->($word_to_complete), + $char_class_before, + $char_class_at_end) +) { + calc_match_coords($self, + $self->{next_row}+1, + $completion); + print stdout "$completion @{$self->{highlight}}\n"; +} + +leave($self); + + + +###################################################################### + +# 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 + $self->{next_row} = -1 if $i < 0; + + return undef; +} + +###################################################################### + +# 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 = $_[0]; + my $current_row = $_[2]; + my $completion; + if ($current_row >= $lines_after_cursor) + { + $completion = shift @{$self->{matches_in_row}}; # get the rightmost one + } + else + { + $completion = pop @{$self->{matches_in_row}}; # get the rightmost one + } + + # check for duplicates + if (exists $self->{already_completed}{$completion}) { + # skip this completion + return find_match(@_); + } else { + $self->{already_completed}{$completion} = 1; + return $completion; + } +} + +###################################################################### + +# 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 //))) + } +} + +###################################################################### + +sub calc_match_coords { + my ($self, $linenum, $completion) = @_; + + my $line = @{$lines} [$linenum]; + my $re = quotemeta $completion; + + $line =~ /$re/; + + #my ($beg_row, $beg_col) = $line->coord_of($-[0]); + #my ($end_row, $end_col) = $line->coord_of($+[0]); + my $beg = $-[0]; + my $end = $+[0]; + + if (exists $self->{highlight}) { + delete $self->{highlight}; + } + # () # TODO: what does () do in perl ???? + + if ($linenum >= $lines_after_cursor) + { + $linenum -= $lines_after_cursor; + } + else + { + $linenum = $last_line - $linenum; + } + + # ACMPL_ISSUE: multi-line completions don't work + # $self->{highlight} = [$beg_row, $beg_col, $end_row, $end_col]; + $self->{highlight} = [$linenum, $beg, $end]; +} + +###################################################################### + +sub leave { + my ($self) = @_; + + delete $self->{next_row}; + delete $self->{matches_in_row}; + delete $self->{already_completed}; + delete $self->{highlight}; +} diff -uraN st-0.8.4/st.c st-autocomplete/st.c --- st-0.8.4/st.c 2021-12-18 10:56:10.545143280 +0400 +++ st-autocomplete/st.c 2021-12-18 13:02:28.898642717 +0400 @@ -17,6 +17,7 @@ #include #include +#include "autocomplete.h" #include "st.h" #include "win.h" @@ -2476,6 +2477,9 @@ return; } + if ( row < term.row || col < term.col ) + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); + /* * slide screen to keep cursor where we expect it - * tscrollup would work here, but we can optimize to @@ -2595,3 +2599,216 @@ 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 char acmpl [1000]; // ACMPL_ISSUE: why 1000? + + static FILE * acmpl_exec = NULL; + static int acmpl_status; + + static const char * stbuffile; + static char target [1000]; // ACMPL_ISSUE: why 1000? dynamically allocate char array of size term.col + static size_t targetlen; + + static char completion [1000] = {0}; // ACMPL_ISSUE: why 1000? dynamically allocate char array of size term.col + static size_t complen_prev = 0; // NOTE: always clear this variable after clearing completion + + static int cx, cy; + +// Check for deactivation + + if (acmpl_cmdindex == ACMPL_DEACTIVATE) + { + +// Deactivate autocomplete mode keeping current completion + + if (active) + { + active = 0; + pclose (acmpl_exec); + remove (stbuffile); + + if (complen_prev) + { + selclear (); + complen_prev = 0; + } + } + + return; + } + +// Check for undo + + if (acmpl_cmdindex == ACMPL_UNDO) + { + +// Deactivate autocomplete mode recovering target + + if (active) + { + active = 0; + pclose (acmpl_exec); + remove (stbuffile); + + if (complen_prev) + { + selclear (); + for (size_t i = 0; i < complen_prev; i++) + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is the right way + complen_prev = 0; + ttywrite (target, targetlen, 0); // ACMPL_ISSUE: I'm not sure that this is a right solution + } + } + + return; + } + +// Check for command change + + if (acmpl_cmdindex != acmpl_cmdindex_prev) + { + +// If command is changed, goto acmpl_begin avoiding rewriting st buffer + + if (active) + { + acmpl_cmdindex_prev = acmpl_cmdindex; + + goto acmpl_begin; + } + } + +// If not active + + if (active == 0) + { + acmpl_cmdindex_prev = acmpl_cmdindex; + cx = term.c.x; + cy = term.c.y; + +// Write st buffer to a temp file + + stbuffile = tmpnam (NULL); // check for return value ... + // ACMPL_ISSUE: use coprocesses instead of temp files + + FILE * stbuf = fopen (stbuffile, "w"); // check for opening error ... + char * stbufline = malloc (term.col + 2); // check for allocating error ... + + for (size_t y = 0; y < term.row; y++) + { + size_t x = 0; + for (; x < term.col; x++) + utf8encode (term.line [y] [x].u, stbufline + x); + stbufline [x] = '\n'; + stbufline [x + 1] = 0; + fputs (stbufline, stbuf); + } + + free (stbufline); + fclose (stbuf); + +acmpl_begin: + +// Run st-autocomplete + + sprintf ( + acmpl, + "cat %100s | st-autocomplete %500s %d %d", // ACMPL_ISSUE: why 100 and 500? + stbuffile, + acmpl_cmd [acmpl_cmdindex], + cy, + cx + ); + + acmpl_exec = popen (acmpl, "r"); // ACMPL_ISSUE: popen isn't defined by The Standard. Does it work in BSDs for example? + // check for popen error ... + +// Read the target, targetlen + + fscanf (acmpl_exec, "%500s\n", target); // check for scanning error ... + targetlen = strlen (target); + } + +// Read a completion if exists (acmpl_status) + + unsigned line, beg, end; + + acmpl_status = fscanf (acmpl_exec, "%500s %u %u %u\n", completion, & line, & beg, & end); + // ACMPL_ISSUE: why 500? use term.col instead + +// Exit if no completions found + + if (active == 0 && acmpl_status == EOF) + { + +// Close st-autocomplete and exit without activating the autocomplete mode + + pclose (acmpl_exec); + remove (stbuffile); + return; + } + +// If completions found, enable autocomplete mode and autocomplete the target + + active = 1; + +// Clear target before first completion + + if (complen_prev == 0) + { + for (size_t i = 0; i < targetlen; i++) + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is a right solution + } + +// Clear previuos completion if this is not the first + + else + { + selclear (); + for (size_t i = 0; i < complen_prev; i++) + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is a right solution + complen_prev = 0; + } + +// If no more completions found, reset and restart + + if (acmpl_status == EOF) + { + active = 0; + pclose (acmpl_exec); + ttywrite (target, targetlen, 0); + goto acmpl_begin; + } + +// Read the new completion and autcomplete + + selstart (beg, line, 0); + selextend (end - 1, line, 1, 0); + xsetsel (getsel ()); + + complen_prev = strlen (completion); + ttywrite (completion, complen_prev, 0); +} diff -uraN st-0.8.4/st.h st-autocomplete/st.h --- st-0.8.4/st.h 2021-12-18 10:56:10.545143280 +0400 +++ st-autocomplete/st.h 2021-12-18 10:56:10.545143280 +0400 @@ -77,6 +77,8 @@ const char *s; } Arg; +void autocomplete (const Arg *); + void die(const char *, ...); void redraw(void); void draw(void); diff -uraN st-0.8.4/x.c st-autocomplete/x.c --- st-0.8.4/x.c 2021-12-18 10:56:10.545143280 +0400 +++ st-autocomplete/x.c 2021-12-18 10:56:10.545143280 +0400 @@ -1803,11 +1803,15 @@ /* 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; } } + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); + /* 2. custom keys from config.h */ if ((customkey = kmap(ksym, e->state))) { ttywrite(customkey, strlen(customkey), 1);