st-autocomplete-20211215-181216-st-0.8.4-testrelease.diff (19137B)
1 diff -uraN st-0.8.4/autocomplete.h st-autocomplete/autocomplete.h 2 --- st-0.8.4/autocomplete.h 1970-01-01 04:00:00.000000000 +0400 3 +++ st-autocomplete/autocomplete.h 2021-12-14 20:20:03.322050025 +0400 4 @@ -0,0 +1,16 @@ 5 +# ifndef __ST_AUTOCOMPLETE_H 6 +# define __ST_AUTOCOMPLETE_H 7 + 8 +enum { 9 + ACMPL_DEACTIVATE, 10 + ACMPL_WORD, 11 + ACMPL_WWORD, 12 + ACMPL_FUZZY_WORD, 13 + ACMPL_FUZZY_WWORD, 14 + ACMPL_FUZZY, 15 + ACMPL_SUFFIX, 16 + ACMPL_SURROUND, 17 + ACMPL_UNDO, 18 +}; 19 + 20 +# endif // __ST_AUTOCOMPLETE_H 21 diff -uraN st-0.8.4/config.def.h st-autocomplete/config.def.h 22 --- st-0.8.4/config.def.h 2021-12-14 20:03:03.981322164 +0400 23 +++ st-autocomplete/config.def.h 2021-12-14 20:22:30.088821478 +0400 24 @@ -168,6 +168,8 @@ 25 */ 26 static uint forcemousemod = ShiftMask; 27 28 +# include "autocomplete.h" 29 + 30 /* 31 * Internal mouse shortcuts. 32 * Beware that overloading Button1 will disable the selection. 33 @@ -199,6 +201,14 @@ 34 { TERMMOD, XK_Y, selpaste, {.i = 0} }, 35 { ShiftMask, XK_Insert, selpaste, {.i = 0} }, 36 { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, 37 + { ControlMask|Mod1Mask, XK_slash, autocomplete, { .i = ACMPL_WORD } }, 38 + { ControlMask|Mod1Mask, XK_period, autocomplete, { .i = ACMPL_FUZZY_WORD } }, 39 + { ControlMask|Mod1Mask, XK_comma, autocomplete, { .i = ACMPL_FUZZY } }, 40 + { ControlMask|Mod1Mask, XK_apostrophe, autocomplete, { .i = ACMPL_SUFFIX } }, 41 + { ControlMask|Mod1Mask, XK_semicolon, autocomplete, { .i = ACMPL_SURROUND } }, 42 + { ControlMask|Mod1Mask, XK_bracketright,autocomplete, { .i = ACMPL_WWORD } }, 43 + { ControlMask|Mod1Mask, XK_bracketleft, autocomplete, { .i = ACMPL_FUZZY_WWORD } }, 44 + { ControlMask|Mod1Mask, XK_equal, autocomplete, { .i = ACMPL_UNDO } }, 45 }; 46 47 /* 48 diff -uraN st-0.8.4/Makefile st-autocomplete/Makefile 49 --- st-0.8.4/Makefile 2021-12-14 20:03:03.981322164 +0400 50 +++ st-autocomplete/Makefile 2021-12-15 07:12:40.291573671 +0400 51 @@ -44,6 +44,8 @@ 52 mkdir -p $(DESTDIR)$(PREFIX)/bin 53 cp -f st $(DESTDIR)$(PREFIX)/bin 54 chmod 755 $(DESTDIR)$(PREFIX)/bin/st 55 + cp -f st-autocomplete $(DESTDIR)$(PREFIX)/bin 56 + chmod 755 $(DESTDIR)$(PREFIX)/bin/st-autocomplete 57 mkdir -p $(DESTDIR)$(MANPREFIX)/man1 58 sed "s/VERSION/$(VERSION)/g" < st.1 > $(DESTDIR)$(MANPREFIX)/man1/st.1 59 chmod 644 $(DESTDIR)$(MANPREFIX)/man1/st.1 60 @@ -52,6 +54,7 @@ 61 62 uninstall: 63 rm -f $(DESTDIR)$(PREFIX)/bin/st 64 + rm -f $(DESTDIR)$(PREFIX)/bin/st-autocomplete 65 rm -f $(DESTDIR)$(MANPREFIX)/man1/st.1 66 67 .PHONY: all options clean dist install uninstall 68 diff -uraN st-0.8.4/st-autocomplete st-autocomplete/st-autocomplete 69 --- st-0.8.4/st-autocomplete 1970-01-01 04:00:00.000000000 +0400 70 +++ st-autocomplete/st-autocomplete 2021-12-14 20:18:13.171971360 +0400 71 @@ -0,0 +1,266 @@ 72 +#!/usr/bin/perl 73 +######################################################################### 74 +# Copyright (C) 2012-2021 Wojciech Siewierski, Gaspar Vardanyan # 75 +# # 76 +# This program is free software: you can redistribute it and/or modify # 77 +# it under the terms of the GNU General Public License as published by # 78 +# the Free Software Foundation, either version 3 of the License, or # 79 +# (at your option) any later version. # 80 +# # 81 +# This program is distributed in the hope that it will be useful, # 82 +# but WITHOUT ANY WARRANTY; without even the implied warranty of # 83 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # 84 +# GNU General Public License for more details. # 85 +# # 86 +# You should have received a copy of the GNU General Public License # 87 +# along with this program. If not, see <http://www.gnu.org/licenses/>. # 88 +######################################################################### 89 + 90 +my ($cmd, $cursor_row, $cursor_column) = @ARGV; 91 + 92 +# A reference to a function that transforms the completed word 93 +# into a regex matching the completions. Usually generated by 94 +# generate_matcher(). 95 +# 96 +# For example 97 +# $fun = generate_matcher(".*"); 98 +# $fun->("foo"); 99 +# would return "f.*o.*o" 100 +# 101 +# In other words, indirectly decides which characters can 102 +# appear in the completion. 103 +my $matcher; 104 + 105 +# A regular expression matching a character before each match. 106 +# For example, it you want to match the text after a 107 +# whitespace, set it to "\s". 108 +my $char_class_before; 109 + 110 +# A regular expression matching every character in the entered 111 +# text that will be used to find matching completions. Usually 112 +# "\w" or similar. 113 +my $char_class_to_complete; 114 + 115 +# A regular expression matching every allowed last character 116 +# of the completion (uses greedy matching). 117 +my $char_class_at_end; 118 + 119 +if ($cmd eq 'word-complete') { 120 + # Basic word completion. Completes the current word 121 + # without any special matching. 122 + $char_class_before = '[^-\w]'; 123 + $matcher = sub { quotemeta shift }; # identity 124 + $char_class_at_end = '[-\w]'; 125 + $char_class_to_complete = '[-\w]'; 126 +} elsif ($cmd eq 'WORD-complete') { 127 + # The same as above but in the Vim meaning of a "WORD" -- 128 + # whitespace delimited. 129 + $char_class_before = '\s'; 130 + $matcher = sub { quotemeta shift }; 131 + $char_class_at_end = '\S'; 132 + $char_class_to_complete = '\S'; 133 +} elsif ($cmd eq 'fuzzy-word-complete' || 134 + $cmd eq 'skeleton-word-complete') { 135 + # Fuzzy completion of the current word. 136 + $char_class_before = '[^-\w]'; 137 + $matcher = generate_matcher('[-\w]*'); 138 + $char_class_at_end = '[-\w]'; 139 + $char_class_to_complete = '[-\w]'; 140 +} elsif ($cmd eq 'fuzzy-WORD-complete') { 141 + # Fuzzy completion of the current WORD. 142 + $char_class_before = '\s'; 143 + $matcher = generate_matcher('\S*'); 144 + $char_class_at_end = '\S'; 145 + $char_class_to_complete = '\S'; 146 +} elsif ($cmd eq 'fuzzy-complete' || 147 + $cmd eq 'skeleton-complete') { 148 + # Fuzzy completion of an arbitrary text. 149 + $char_class_before = '\W'; 150 + $matcher = generate_matcher('.*?'); 151 + $char_class_at_end = '\w'; 152 + $char_class_to_complete = '\S'; 153 +} elsif ($cmd eq 'suffix-complete') { 154 + # Fuzzy completion of an completing suffixes, like 155 + # completing test=hello from /blah/hello. 156 + $char_class_before = '\S'; 157 + $matcher = generate_matcher('\S*'); 158 + $char_class_at_end = '\S'; 159 + $char_class_to_complete = '\S'; 160 +} elsif ($cmd eq 'surround-complete') { 161 + # Completing contents of quotes and braces. 162 + 163 + # Here we are using three named groups: s, b, p for quotes, braces 164 + # and parenthesis. 165 + $char_class_before = '((?<q>["\'`])|(?<b>\[)|(?<p>\())'; 166 + 167 + $matcher = generate_matcher('.*?'); 168 + 169 + # Here we match text till enclosing pair, using perl conditionals in 170 + # regexps (?(condition)yes-expression|no-expression). 171 + # \0 is used to hack concatenation with '*' later in the code. 172 + $char_class_at_end = '.*?(.(?=(?(<b>)\]|((?(<p>)\)|\g{q})))))\0'; 173 + $char_class_to_complete = '\S'; 174 +} 175 + 176 +my $lines = []; 177 + 178 +while (<STDIN>) 179 +{ 180 + push @{$lines}, $_; 181 +} 182 + 183 +# read the word behind the cursor 184 +$_ = substr(@{$lines} [$cursor_row], 0, $cursor_column); # get the current line up to the cursor... 185 +s/.*?($char_class_to_complete*)$/$1/; # ...and read the last word from it 186 +my $word_to_complete = $_; 187 + 188 +# ignore the completed word itself 189 +$self->{already_completed}{$word_to_complete} = 1; 190 + 191 +print stdout "$word_to_complete\n"; 192 + 193 +# search for matches 194 +while (my $completion = find_match($self, 195 + $word_to_complete, 196 + $self->{next_row} // $cursor_row, 197 + $matcher->($word_to_complete), 198 + $char_class_before, 199 + $char_class_at_end) 200 +) { 201 + calc_match_coords($self, 202 + $self->{next_row}+1, 203 + $completion); 204 + print stdout "$completion @{$self->{highlight}}\n"; 205 +} 206 + 207 +leave($self); 208 + 209 + 210 + 211 +###################################################################### 212 + 213 +# Finds the next matching completion in the row current row or above 214 +# while skipping duplicates using skip_duplicates(). 215 +sub find_match { 216 + my ($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end) = @_; 217 + $self->{matches_in_row} //= []; 218 + 219 + # cycle through all the matches in the current row if not starting a new search 220 + if (@{$self->{matches_in_row}}) { 221 + return skip_duplicates($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end); 222 + } 223 + 224 + 225 + my $i; 226 + # search through all the rows starting with current one or one above the last checked 227 + for ($i = $current_row; $i >= 0; --$i) { 228 + my $line = @{$lines} [$i]; # get the line of text from the row 229 + 230 + if ($i == $cursor_row) { 231 + $line = substr $line, 0, $cursor_column; 232 + } 233 + 234 + $_ = $line; 235 + 236 + # find all the matches in the current line 237 + my $match; 238 + push @{$self->{matches_in_row}}, $+{match} while ($_, $match) = / 239 + (.*${char_class_before}) 240 + (?<match> 241 + ${regexp} 242 + ${char_class_at_end}* 243 + ) 244 + /ix; 245 + # corner case: match at the very beginning of line 246 + push @{$self->{matches_in_row}}, $+{match} if $line =~ /^(${char_class_before}){0}(?<match>$regexp$char_class_at_end*)/i; 247 + 248 + if (@{$self->{matches_in_row}}) { 249 + # remember which row should be searched next 250 + $self->{next_row} = --$i; 251 + 252 + # arguments needed for find_match() mutual recursion 253 + return skip_duplicates($self, $word_to_match, $i, $regexp, $char_class_before, $char_class_at_end); 254 + } 255 + } 256 + 257 + # no more possible completions, revert to the original word 258 + $self->{next_row} = -1 if $i < 0; 259 + 260 + return undef; 261 +} 262 + 263 +###################################################################### 264 + 265 +# Checks whether the completion found by find_match() was already 266 +# found and if it was, calls find_match() again to find the next 267 +# completion. 268 +# 269 +# Takes all the arguments that find_match() would take, to make a 270 +# mutually recursive call. 271 +sub skip_duplicates { 272 + my $self = $_[0]; 273 + my $completion = shift @{$self->{matches_in_row}}; # get the rightmost one 274 + 275 + # check for duplicates 276 + if (exists $self->{already_completed}{$completion}) { 277 + # skip this completion 278 + return find_match(@_); 279 + } else { 280 + $self->{already_completed}{$completion} = 1; 281 + return $completion; 282 + } 283 +} 284 + 285 +###################################################################### 286 + 287 +# Returns a function that takes a string and returns that string with 288 +# this function's argument inserted between its every two characters. 289 +# The resulting string is used as a regular expression matching the 290 +# completion candidates. 291 +sub generate_matcher { 292 + my $regex_between = shift; 293 + 294 + sub { 295 + $_ = shift; 296 + 297 + # sorry for this lispy code, I couldn't resist ;) 298 + (join "$regex_between", 299 + (map quotemeta, 300 + (split //))) 301 + } 302 +} 303 + 304 +###################################################################### 305 + 306 +sub calc_match_coords { 307 + my ($self, $linenum, $completion) = @_; 308 + 309 + my $line = @{$lines} [$linenum]; 310 + my $re = quotemeta $completion; 311 + 312 + $line =~ /$re/; 313 + 314 + #my ($beg_row, $beg_col) = $line->coord_of($-[0]); 315 + #my ($end_row, $end_col) = $line->coord_of($+[0]); 316 + my $beg = $-[0]; 317 + my $end = $+[0]; 318 + 319 + if (exists $self->{highlight}) { 320 + delete $self->{highlight}; 321 + } 322 + # () # TODO: what does () do in perl ???? 323 + 324 + # $self->{highlight} = [$beg_row, $beg_col, $end_row, $end_col]; 325 + $self->{highlight} = [$linenum, $beg, $end]; 326 +} 327 + 328 +###################################################################### 329 + 330 +sub leave { 331 + my ($self) = @_; 332 + 333 + delete $self->{next_row}; 334 + delete $self->{matches_in_row}; 335 + delete $self->{already_completed}; 336 + delete $self->{highlight}; 337 +} 338 diff -uraN st-0.8.4/st.c st-autocomplete/st.c 339 --- st-0.8.4/st.c 2021-12-14 20:03:03.981322164 +0400 340 +++ st-autocomplete/st.c 2021-12-15 07:44:05.609586643 +0400 341 @@ -17,6 +17,7 @@ 342 #include <unistd.h> 343 #include <wchar.h> 344 345 +#include "autocomplete.h" 346 #include "st.h" 347 #include "win.h" 348 349 @@ -2476,6 +2477,9 @@ 350 return; 351 } 352 353 + if ( row < term.row || col < term.col ) 354 + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); 355 + 356 /* 357 * slide screen to keep cursor where we expect it - 358 * tscrollup would work here, but we can optimize to 359 @@ -2595,3 +2599,211 @@ 360 tfulldirt(); 361 draw(); 362 } 363 + 364 +void autocomplete (const Arg * arg) 365 +{ 366 + static _Bool active = 0; 367 + 368 + int acmpl_cmdindex = arg -> i; 369 + 370 + static int acmpl_cmdindex_prev; 371 + 372 + if (active == 0) 373 + acmpl_cmdindex_prev = acmpl_cmdindex; 374 + 375 + static const char * const (acmpl_cmd []) = { 376 + [ACMPL_DEACTIVATE] = "__DEACTIVATE__", 377 + [ACMPL_WORD] = "word-complete", 378 + [ACMPL_WWORD] = "WORD-complete", 379 + [ACMPL_FUZZY_WORD] = "fuzzy-word-complete", 380 + [ACMPL_FUZZY_WWORD] = "fuzzy-WORD-complete", 381 + [ACMPL_FUZZY] = "fuzzy-complete", 382 + [ACMPL_SUFFIX] = "suffix-complete", 383 + [ACMPL_SURROUND] = "surround-complete", 384 + [ACMPL_UNDO] = "__UNDO__", 385 + }; 386 + 387 + static char acmpl [1000]; // ACMPL_ISSUE: why 1000? 388 + 389 + static FILE * acmpl_exec = NULL; 390 + static int acmpl_status; 391 + 392 + static const char * stbuffile; 393 + static char target [1000]; // ACMPL_ISSUE: why 1000? dynamically allocate char array of size term.col 394 + static size_t targetlen; 395 + 396 + static char completion [1000] = {0}; // ACMPL_ISSUE: why 1000? dynamically allocate char array of size term.col 397 + static size_t complen_prev = 0; // NOTE: always clear this variable after clearing completion 398 + 399 +// Check for deactivation 400 + 401 + if (acmpl_cmdindex == ACMPL_DEACTIVATE) 402 + { 403 + 404 +// Deactivate autocomplete mode keeping current completion 405 + 406 + if (active) 407 + { 408 + active = 0; 409 + pclose (acmpl_exec); 410 + remove (stbuffile); 411 + 412 + if (complen_prev) 413 + { 414 + selclear (); 415 + complen_prev = 0; 416 + } 417 + } 418 + 419 + return; 420 + } 421 + 422 +// Check for undo 423 + 424 + if (acmpl_cmdindex == ACMPL_UNDO) 425 + { 426 + 427 +// Deactivate autocomplete mode recovering target 428 + 429 + if (active) 430 + { 431 + active = 0; 432 + pclose (acmpl_exec); 433 + remove (stbuffile); 434 + 435 + if (complen_prev) 436 + { 437 + selclear (); 438 + for (size_t i = 0; i < complen_prev; i++) 439 + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is the right way 440 + complen_prev = 0; 441 + ttywrite (target, targetlen, 0); // ACMPL_ISSUE: I'm not sure that this is a right solution 442 + } 443 + } 444 + 445 + return; 446 + } 447 + 448 +// Check for command change 449 + 450 + if (acmpl_cmdindex != acmpl_cmdindex_prev) 451 + { 452 + 453 +// If command is changed, goto acmpl_begin avoiding rewriting st buffer 454 + 455 + if (active) 456 + { 457 + acmpl_cmdindex_prev = acmpl_cmdindex; 458 + 459 + goto acmpl_begin; 460 + } 461 + } 462 + 463 +// If not active 464 + 465 + if (active == 0) 466 + { 467 + acmpl_cmdindex_prev = acmpl_cmdindex; 468 + 469 +// Write st buffer to a temp file 470 + 471 + stbuffile = tmpnam (NULL); // check for return value ... 472 + // ACMPL_ISSUE: use coprocesses instead of temp files 473 + sprintf ( 474 + acmpl, 475 + "cat %100s | st-autocomplete %500s %d %d", // ACMPL_ISSUE: why 100 and 500? 476 + stbuffile, 477 + acmpl_cmd [acmpl_cmdindex], 478 + term.c.y, 479 + term.c.x 480 + ); 481 + 482 + FILE * stbuf = fopen (stbuffile, "w"); // check for opening error ... 483 + char * stbufline = malloc (term.col + 2); // check for allocating error ... 484 + 485 + for (size_t y = 0; y < term.row; y++) 486 + { 487 + size_t x = 0; 488 + for (; x < term.col; x++) 489 + utf8encode (term.line [y] [x].u, stbufline + x); 490 + stbufline [x] = '\n'; 491 + stbufline [x + 1] = 0; 492 + fputs (stbufline, stbuf); 493 + } 494 + 495 + free (stbufline); 496 + fclose (stbuf); 497 + 498 +acmpl_begin: 499 + 500 +// Run st-autocomplete 501 + 502 + acmpl_exec = popen (acmpl, "r"); // ACMPL_ISSUE: popen isn't defined by The Standard. Does it work in BSDs for example? 503 + // check for popen error ... 504 + 505 +// Read the target, targetlen 506 + 507 + fscanf (acmpl_exec, "%500s\n", target); // check for scanning error ... 508 + targetlen = strlen (target); 509 + } 510 + 511 +// Read a completion if exists (acmpl_status) 512 + 513 + unsigned line, beg, end; 514 + 515 + acmpl_status = fscanf (acmpl_exec, "%500s %u %u %u\n", completion, & line, & beg, & end); 516 + // ACMPL_ISSUE: why 500? use term.col instead 517 + 518 +// Exit if no completions found 519 + 520 + if (active == 0 && acmpl_status == EOF) 521 + { 522 + 523 +// Close st-autocomplete and exit without activating the autocomplete mode 524 + 525 + pclose (acmpl_exec); 526 + remove (stbuffile); 527 + return; 528 + } 529 + 530 +// If completions found, enable autocomplete mode and autocomplete the target 531 + 532 + active = 1; 533 + 534 +// Clear target before first completion 535 + 536 + if (complen_prev == 0) 537 + { 538 + for (size_t i = 0; i < targetlen; i++) 539 + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is a right solution 540 + } 541 + 542 +// Clear previuos completion if this is not the first 543 + 544 + else 545 + { 546 + selclear (); 547 + for (size_t i = 0; i < complen_prev; i++) 548 + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is a right solution 549 + complen_prev = 0; 550 + } 551 + 552 +// If no more completions found, reset and restart 553 + 554 + if (acmpl_status == EOF) 555 + { 556 + active = 0; 557 + pclose (acmpl_exec); 558 + ttywrite (target, targetlen, 0); 559 + goto acmpl_begin; 560 + } 561 + 562 +// Read the new completion and autcomplete 563 + 564 + selstart (beg, line, 0); 565 + selextend (end - 1, line, 1, 0); 566 + xsetsel (getsel ()); 567 + 568 + complen_prev = strlen (completion); 569 + ttywrite (completion, complen_prev, 0); 570 +} 571 diff -uraN st-0.8.4/st.h st-autocomplete/st.h 572 --- st-0.8.4/st.h 2021-12-14 20:03:03.981322164 +0400 573 +++ st-autocomplete/st.h 2021-12-14 20:28:59.272432749 +0400 574 @@ -77,6 +77,8 @@ 575 const char *s; 576 } Arg; 577 578 +void autocomplete (const Arg *); 579 + 580 void die(const char *, ...); 581 void redraw(void); 582 void draw(void); 583 diff -uraN st-0.8.4/x.c st-autocomplete/x.c 584 --- st-0.8.4/x.c 2021-12-14 20:03:03.981322164 +0400 585 +++ st-autocomplete/x.c 2021-12-14 20:30:30.045830893 +0400 586 @@ -1803,11 +1803,15 @@ 587 /* 1. shortcuts */ 588 for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) { 589 if (ksym == bp->keysym && match(bp->mod, e->state)) { 590 + if (bp -> func != autocomplete) 591 + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); 592 bp->func(&(bp->arg)); 593 return; 594 } 595 } 596 597 + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); 598 + 599 /* 2. custom keys from config.h */ 600 if ((customkey = kmap(ksym, e->state))) { 601 ttywrite(customkey, strlen(customkey), 1);