From 650869359d3568dd2a000d474054e835a9c7ac74 Mon Sep 17 00:00:00 2001 From: elbachir-one Date: Wed, 3 Jul 2024 22:44:40 +0100 Subject: [PATCH] The use of mkstemp' --- Makefile | 3 + autocomplete.h | 16 +++ config.def.h | 12 ++ st-autocomplete | 310 ++++++++++++++++++++++++++++++++++++++++++++++++ st.c | 227 +++++++++++++++++++++++++++++++++++ st.h | 2 + x.c | 9 ++ 7 files changed, 579 insertions(+) create mode 100644 autocomplete.h create mode 100644 st-autocomplete diff --git a/Makefile b/Makefile index 93fed02..9aff9e0 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,8 @@ install: st mkdir -p $(DESTDIR)$(PREFIX)/bin cp -f st $(DESTDIR)$(PREFIX)/bin chmod 755 $(DESTDIR)$(PREFIX)/bin/st + cp -f st-autocomplete $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/st-autocomplete mkdir -p $(DESTDIR)$(MANPREFIX)/man1 sed "s/VERSION/$(VERSION)/g" < st.1 > $(DESTDIR)$(MANPREFIX)/man1/st.1 chmod 644 $(DESTDIR)$(MANPREFIX)/man1/st.1 @@ -46,6 +48,7 @@ install: st uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/st + rm -f $(DESTDIR)$(PREFIX)/bin/st-autocomplete rm -f $(DESTDIR)$(MANPREFIX)/man1/st.1 .PHONY: all clean dist install uninstall diff --git a/autocomplete.h b/autocomplete.h new file mode 100644 index 0000000..fc88447 --- /dev/null +++ b/autocomplete.h @@ -0,0 +1,16 @@ +# ifndef __ST_AUTOCOMPLETE_H +# define __ST_AUTOCOMPLETE_H + +enum { + ACMPL_DEACTIVATE, + ACMPL_WORD, + ACMPL_WWORD, + ACMPL_FUZZY_WORD, + ACMPL_FUZZY_WWORD, + ACMPL_FUZZY, + ACMPL_SUFFIX, + ACMPL_SURROUND, + ACMPL_UNDO, +}; + +# endif // __ST_AUTOCOMPLETE_H diff --git a/config.def.h b/config.def.h index 2cd740a..b74e03e 100644 --- a/config.def.h +++ b/config.def.h @@ -170,6 +170,8 @@ static unsigned int defaultattr = 11; */ static uint forcemousemod = ShiftMask; +#include "autocomplete.h" + /* * Internal mouse shortcuts. * Beware that overloading Button1 will disable the selection. @@ -187,6 +189,8 @@ static MouseShortcut mshortcuts[] = { #define MODKEY Mod1Mask #define TERMMOD (ControlMask|ShiftMask) +#define ACMPL_MOD ControlMask|Mod1Mask + static Shortcut shortcuts[] = { /* mask keysym function argument */ { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, @@ -201,6 +205,14 @@ static Shortcut shortcuts[] = { { TERMMOD, XK_Y, selpaste, {.i = 0} }, { ShiftMask, XK_Insert, selpaste, {.i = 0} }, { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, + { ACMPL_MOD, XK_slash, autocomplete, { .i = ACMPL_WORD } }, + { ACMPL_MOD, XK_period, autocomplete, { .i = ACMPL_FUZZY_WORD } }, + { ACMPL_MOD, XK_comma, autocomplete, { .i = ACMPL_FUZZY } }, + { ACMPL_MOD, XK_apostrophe, autocomplete, { .i = ACMPL_SUFFIX } }, + { ACMPL_MOD, XK_semicolon, autocomplete, { .i = ACMPL_SURROUND } }, + { ACMPL_MOD, XK_bracketright,autocomplete, { .i = ACMPL_WWORD } }, + { ACMPL_MOD, XK_bracketleft, autocomplete, { .i = ACMPL_FUZZY_WWORD } }, + { ACMPL_MOD, XK_equal, autocomplete, { .i = ACMPL_UNDO } }, }; /* diff --git a/st-autocomplete b/st-autocomplete new file mode 100644 index 0000000..0fad536 --- /dev/null +++ b/st-autocomplete @@ -0,0 +1,310 @@ +#!/usr/bin/perl +######################################################################### +# Copyright (C) 2012-2017 Wojciech Siewierski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +######################################################################### + +my ($cmd, $cursor_row, $cursor_column) = @ARGV; + +my $lines = []; +my $lines1 = []; + +my $last_line = -1; +my $lines_before_cursor = 0; + +while () +{ + $last_line++; + + s/[^[:print:]]/?/g; + + if ($last_line < $cursor_row) + { + unshift @{$lines1}, $_; + $lines_before_cursor++; + } + else + { + unshift @{$lines}, $_; + } +} + +foreach (@{$lines1}) +{ + unshift @{$lines}, $_; +} + +my $cursor_row_in = $cursor_row; + +$cursor_row = $last_line; + + +$self = {}; + +# A reference to a function that transforms the completed word +# into a regex matching the completions. Usually generated by +# generate_matcher(). +# +# For example +# $fun = generate_matcher(".*"); +# $fun->("foo"); +# would return "f.*o.*o" +# +# In other words, indirectly decides which characters can +# appear in the completion. +my $matcher; + +# A regular expression matching a character before each match. +# For example, it you want to match the text after a +# whitespace, set it to "\s". +my $char_class_before; + +# A regular expression matching every character in the entered +# text that will be used to find matching completions. Usually +# "\w" or similar. +my $char_class_to_complete; + +# A regular expression matching every allowed last character +# of the completion (uses greedy matching). +my $char_class_at_end; + +if ($cmd eq 'word-complete') { + # Basic word completion. Completes the current word + # without any special matching. + $char_class_before = '[^-\w]'; + $matcher = sub { quotemeta shift }; # identity + $char_class_at_end = '[-\w]'; + $char_class_to_complete = '[-\w]'; +} elsif ($cmd eq 'WORD-complete') { + # The same as above but in the Vim meaning of a "WORD" -- + # whitespace delimited. + $char_class_before = '\s'; + $matcher = sub { quotemeta shift }; + $char_class_at_end = '\S'; + $char_class_to_complete = '\S'; +} elsif ($cmd eq 'fuzzy-word-complete' || + $cmd eq 'skeleton-word-complete') { + # Fuzzy completion of the current word. + $char_class_before = '[^-\w]'; + $matcher = generate_matcher('[-\w]*'); + $char_class_at_end = '[-\w]'; + $char_class_to_complete = '[-\w]'; +} elsif ($cmd eq 'fuzzy-WORD-complete') { + # Fuzzy completion of the current WORD. + $char_class_before = '\s'; + $matcher = generate_matcher('\S*'); + $char_class_at_end = '\S'; + $char_class_to_complete = '\S'; +} elsif ($cmd eq 'fuzzy-complete' || + $cmd eq 'skeleton-complete') { + # Fuzzy completion of an arbitrary text. + $char_class_before = '\W'; + $matcher = generate_matcher('.*?'); + $char_class_at_end = '\w'; + $char_class_to_complete = '\S'; +} elsif ($cmd eq 'suffix-complete') { + # Fuzzy completion of an completing suffixes, like + # completing test=hello from /blah/hello. + $char_class_before = '\S'; + $matcher = generate_matcher('\S*'); + $char_class_at_end = '\S'; + $char_class_to_complete = '\S'; +} elsif ($cmd eq 'surround-complete') { + # Completing contents of quotes and braces. + + # Here we are using three named groups: s, b, p for quotes, braces + # and parenthesis. + $char_class_before = '((?["\'`])|(?\[)|(?

\())'; + + $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 57c6e96..9ff8d00 100644 --- a/st.c +++ b/st.c @@ -17,6 +17,7 @@ #include #include +#include "autocomplete.h" #include "st.h" #include "win.h" @@ -2557,6 +2558,8 @@ tresize(int col, int row) return; } + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); + /* * slide screen to keep cursor where we expect it - * tscrollup would work here, but we can optimize to @@ -2676,3 +2679,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 fd3b0d8..113ebad 100644 --- a/st.h +++ b/st.h @@ -77,6 +77,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 bd23686..c647721 100644 --- a/x.c +++ b/x.c @@ -1859,11 +1859,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); -- 2.45.2