["\'`])|(?\[)|(?\())'; + + $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 -uraN st-0.8.5/st.c st-autocomplete/st.c --- st-0.8.5/st.c 2022-03-13 02:45:34.586842452 +0400 +++ st-autocomplete/st.c 2022-03-27 22:28:39.041693478 +0400 @@ -17,6 +17,7 @@ #include #include +#include "autocomplete.h" #include "st.h" #include "win.h" @@ -2569,6 +2570,8 @@ return; } + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); + /* * slide screen to keep cursor where we expect it - * tscrollup would work here, but we can optimize to @@ -2688,3 +2691,256 @@ 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; + + // ACMPL_ISSUE: crashes when term.row is too small + +// 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); // ACMPL_ISSUE: check for return value ... + // ACMPL_ISSUE: use coprocesses instead of temp files + + FILE * stbuf = fopen (stbuffile, "w"); // ACMPL_ISSUE: check for opening error ... + char * stbufline = malloc (term.col + 2); // ACMPL_ISSUE: check for allocating error ... + + 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: + +// 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? + // ACMPL_ISSUE: check for popen error ... + +// Read the target, targetlen + + fscanf (acmpl_exec, "%500s\n", target); // ACMPL_ISSUE: check for scanning error ... + targetlen = strlen (target); + } + +// Read a completion if exists (acmpl_status) + + unsigned line, beg, end; + + acmpl_status = fscanf (acmpl_exec, "%500[^\n]\n%u\n%u\n%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; + } + +// Count wrapped lines before the current line + + 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++; + } + +// Autcomplete + + complen_prev = strlen (completion); + ttywrite (completion, complen_prev, 0); + + if (line == cy && beg > cx) + { + beg += complen_prev - targetlen; + end += complen_prev - targetlen; + + // ACMPL_ISSUE: highlignthing doesn't work when "line == cy && beg > cx", + // but coordinates are correct... + } + + end--; + + selstart (beg % term.col, line + wl + beg / term.col, 0); + selextend (end % term.col, line + wl + end / term.col, 1, 0); + xsetsel (getsel ()); +} diff -uraN st-0.8.5/st.h st-autocomplete/st.h --- st-0.8.5/st.h 2022-03-13 02:45:34.586842452 +0400 +++ st-autocomplete/st.h 2022-03-13 02:45:34.586842452 +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.5/x.c st-autocomplete/x.c --- st-0.8.5/x.c 2022-03-13 02:45:34.586842452 +0400 +++ st-autocomplete/x.c 2022-03-13 02:45:34.590175835 +0400 @@ -1834,11 +1834,20 @@ /* 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);