st-autocomplete-20211218-131244-st-0.8.4-testrelease.diff (19768B)
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-18 10:56:10.545143280 +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-18 10:56:10.545143280 +0400 23 +++ st-autocomplete/config.def.h 2021-12-18 10:56:10.545143280 +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-18 10:56:10.545143280 +0400 50 +++ st-autocomplete/Makefile 2021-12-18 10:56:10.545143280 +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-18 12:42:16.841949553 +0400 71 @@ -0,0 +1,300 @@ 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 +my $last_line = -1; 179 +my $lines_after_cursor = 0; 180 + 181 +while (<STDIN>) 182 +{ 183 + $last_line++; 184 + 185 + if ($last_line <= $cursor_row) 186 + { 187 + push @{$lines}, $_; 188 + } 189 + else 190 + { 191 + unshift @{$lines}, $_; 192 + $lines_after_cursor++; 193 + } 194 +} 195 + 196 +$cursor_row = $last_line; 197 + 198 +# read the word behind the cursor 199 +$_ = substr(@{$lines} [$cursor_row], 0, $cursor_column); # get the current line up to the cursor... 200 +s/.*?($char_class_to_complete*)$/$1/; # ...and read the last word from it 201 +my $word_to_complete = quotemeta; 202 + 203 +# ignore the completed word itself 204 +$self->{already_completed}{$word_to_complete} = 1; 205 + 206 +print stdout "$word_to_complete\n"; 207 + 208 +# search for matches 209 +while (my $completion = find_match($self, 210 + $word_to_complete, 211 + $self->{next_row} // $cursor_row, 212 + $matcher->($word_to_complete), 213 + $char_class_before, 214 + $char_class_at_end) 215 +) { 216 + calc_match_coords($self, 217 + $self->{next_row}+1, 218 + $completion); 219 + print stdout "$completion @{$self->{highlight}}\n"; 220 +} 221 + 222 +leave($self); 223 + 224 + 225 + 226 +###################################################################### 227 + 228 +# Finds the next matching completion in the row current row or above 229 +# while skipping duplicates using skip_duplicates(). 230 +sub find_match { 231 + my ($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end) = @_; 232 + $self->{matches_in_row} //= []; 233 + 234 + # cycle through all the matches in the current row if not starting a new search 235 + if (@{$self->{matches_in_row}}) { 236 + return skip_duplicates($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end); 237 + } 238 + 239 + 240 + my $i; 241 + # search through all the rows starting with current one or one above the last checked 242 + for ($i = $current_row; $i >= 0; --$i) { 243 + my $line = @{$lines} [$i]; # get the line of text from the row 244 + 245 + if ($i == $cursor_row) { 246 + $line = substr $line, 0, $cursor_column; 247 + } 248 + 249 + $_ = $line; 250 + 251 + # find all the matches in the current line 252 + my $match; 253 + push @{$self->{matches_in_row}}, $+{match} while ($_, $match) = / 254 + (.*${char_class_before}) 255 + (?<match> 256 + ${regexp} 257 + ${char_class_at_end}* 258 + ) 259 + /ix; 260 + # corner case: match at the very beginning of line 261 + push @{$self->{matches_in_row}}, $+{match} if $line =~ /^(${char_class_before}){0}(?<match>$regexp$char_class_at_end*)/i; 262 + 263 + if (@{$self->{matches_in_row}}) { 264 + # remember which row should be searched next 265 + $self->{next_row} = --$i; 266 + 267 + # arguments needed for find_match() mutual recursion 268 + return skip_duplicates($self, $word_to_match, $i, $regexp, $char_class_before, $char_class_at_end); 269 + } 270 + } 271 + 272 + # no more possible completions, revert to the original word 273 + $self->{next_row} = -1 if $i < 0; 274 + 275 + return undef; 276 +} 277 + 278 +###################################################################### 279 + 280 +# Checks whether the completion found by find_match() was already 281 +# found and if it was, calls find_match() again to find the next 282 +# completion. 283 +# 284 +# Takes all the arguments that find_match() would take, to make a 285 +# mutually recursive call. 286 +sub skip_duplicates { 287 + my $self = $_[0]; 288 + my $current_row = $_[2]; 289 + my $completion; 290 + if ($current_row >= $lines_after_cursor) 291 + { 292 + $completion = shift @{$self->{matches_in_row}}; # get the rightmost one 293 + } 294 + else 295 + { 296 + $completion = pop @{$self->{matches_in_row}}; # get the rightmost one 297 + } 298 + 299 + # check for duplicates 300 + if (exists $self->{already_completed}{$completion}) { 301 + # skip this completion 302 + return find_match(@_); 303 + } else { 304 + $self->{already_completed}{$completion} = 1; 305 + return $completion; 306 + } 307 +} 308 + 309 +###################################################################### 310 + 311 +# Returns a function that takes a string and returns that string with 312 +# this function's argument inserted between its every two characters. 313 +# The resulting string is used as a regular expression matching the 314 +# completion candidates. 315 +sub generate_matcher { 316 + my $regex_between = shift; 317 + 318 + sub { 319 + $_ = shift; 320 + 321 + # sorry for this lispy code, I couldn't resist ;) 322 + (join "$regex_between", 323 + (map quotemeta, 324 + (split //))) 325 + } 326 +} 327 + 328 +###################################################################### 329 + 330 +sub calc_match_coords { 331 + my ($self, $linenum, $completion) = @_; 332 + 333 + my $line = @{$lines} [$linenum]; 334 + my $re = quotemeta $completion; 335 + 336 + $line =~ /$re/; 337 + 338 + #my ($beg_row, $beg_col) = $line->coord_of($-[0]); 339 + #my ($end_row, $end_col) = $line->coord_of($+[0]); 340 + my $beg = $-[0]; 341 + my $end = $+[0]; 342 + 343 + if (exists $self->{highlight}) { 344 + delete $self->{highlight}; 345 + } 346 + # () # TODO: what does () do in perl ???? 347 + 348 + if ($linenum >= $lines_after_cursor) 349 + { 350 + $linenum -= $lines_after_cursor; 351 + } 352 + else 353 + { 354 + $linenum = $last_line - $linenum; 355 + } 356 + 357 + # ACMPL_ISSUE: multi-line completions don't work 358 + # $self->{highlight} = [$beg_row, $beg_col, $end_row, $end_col]; 359 + $self->{highlight} = [$linenum, $beg, $end]; 360 +} 361 + 362 +###################################################################### 363 + 364 +sub leave { 365 + my ($self) = @_; 366 + 367 + delete $self->{next_row}; 368 + delete $self->{matches_in_row}; 369 + delete $self->{already_completed}; 370 + delete $self->{highlight}; 371 +} 372 diff -uraN st-0.8.4/st.c st-autocomplete/st.c 373 --- st-0.8.4/st.c 2021-12-18 10:56:10.545143280 +0400 374 +++ st-autocomplete/st.c 2021-12-18 13:02:28.898642717 +0400 375 @@ -17,6 +17,7 @@ 376 #include <unistd.h> 377 #include <wchar.h> 378 379 +#include "autocomplete.h" 380 #include "st.h" 381 #include "win.h" 382 383 @@ -2476,6 +2477,9 @@ 384 return; 385 } 386 387 + if ( row < term.row || col < term.col ) 388 + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); 389 + 390 /* 391 * slide screen to keep cursor where we expect it - 392 * tscrollup would work here, but we can optimize to 393 @@ -2595,3 +2599,216 @@ 394 tfulldirt(); 395 draw(); 396 } 397 + 398 +void autocomplete (const Arg * arg) 399 +{ 400 + static _Bool active = 0; 401 + 402 + int acmpl_cmdindex = arg -> i; 403 + 404 + static int acmpl_cmdindex_prev; 405 + 406 + if (active == 0) 407 + acmpl_cmdindex_prev = acmpl_cmdindex; 408 + 409 + static const char * const (acmpl_cmd []) = { 410 + [ACMPL_DEACTIVATE] = "__DEACTIVATE__", 411 + [ACMPL_WORD] = "word-complete", 412 + [ACMPL_WWORD] = "WORD-complete", 413 + [ACMPL_FUZZY_WORD] = "fuzzy-word-complete", 414 + [ACMPL_FUZZY_WWORD] = "fuzzy-WORD-complete", 415 + [ACMPL_FUZZY] = "fuzzy-complete", 416 + [ACMPL_SUFFIX] = "suffix-complete", 417 + [ACMPL_SURROUND] = "surround-complete", 418 + [ACMPL_UNDO] = "__UNDO__", 419 + }; 420 + 421 + static char acmpl [1000]; // ACMPL_ISSUE: why 1000? 422 + 423 + static FILE * acmpl_exec = NULL; 424 + static int acmpl_status; 425 + 426 + static const char * stbuffile; 427 + static char target [1000]; // ACMPL_ISSUE: why 1000? dynamically allocate char array of size term.col 428 + static size_t targetlen; 429 + 430 + static char completion [1000] = {0}; // ACMPL_ISSUE: why 1000? dynamically allocate char array of size term.col 431 + static size_t complen_prev = 0; // NOTE: always clear this variable after clearing completion 432 + 433 + static int cx, cy; 434 + 435 +// Check for deactivation 436 + 437 + if (acmpl_cmdindex == ACMPL_DEACTIVATE) 438 + { 439 + 440 +// Deactivate autocomplete mode keeping current completion 441 + 442 + if (active) 443 + { 444 + active = 0; 445 + pclose (acmpl_exec); 446 + remove (stbuffile); 447 + 448 + if (complen_prev) 449 + { 450 + selclear (); 451 + complen_prev = 0; 452 + } 453 + } 454 + 455 + return; 456 + } 457 + 458 +// Check for undo 459 + 460 + if (acmpl_cmdindex == ACMPL_UNDO) 461 + { 462 + 463 +// Deactivate autocomplete mode recovering target 464 + 465 + if (active) 466 + { 467 + active = 0; 468 + pclose (acmpl_exec); 469 + remove (stbuffile); 470 + 471 + if (complen_prev) 472 + { 473 + selclear (); 474 + for (size_t i = 0; i < complen_prev; i++) 475 + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is the right way 476 + complen_prev = 0; 477 + ttywrite (target, targetlen, 0); // ACMPL_ISSUE: I'm not sure that this is a right solution 478 + } 479 + } 480 + 481 + return; 482 + } 483 + 484 +// Check for command change 485 + 486 + if (acmpl_cmdindex != acmpl_cmdindex_prev) 487 + { 488 + 489 +// If command is changed, goto acmpl_begin avoiding rewriting st buffer 490 + 491 + if (active) 492 + { 493 + acmpl_cmdindex_prev = acmpl_cmdindex; 494 + 495 + goto acmpl_begin; 496 + } 497 + } 498 + 499 +// If not active 500 + 501 + if (active == 0) 502 + { 503 + acmpl_cmdindex_prev = acmpl_cmdindex; 504 + cx = term.c.x; 505 + cy = term.c.y; 506 + 507 +// Write st buffer to a temp file 508 + 509 + stbuffile = tmpnam (NULL); // check for return value ... 510 + // ACMPL_ISSUE: use coprocesses instead of temp files 511 + 512 + FILE * stbuf = fopen (stbuffile, "w"); // check for opening error ... 513 + char * stbufline = malloc (term.col + 2); // check for allocating error ... 514 + 515 + for (size_t y = 0; y < term.row; y++) 516 + { 517 + size_t x = 0; 518 + for (; x < term.col; x++) 519 + utf8encode (term.line [y] [x].u, stbufline + x); 520 + stbufline [x] = '\n'; 521 + stbufline [x + 1] = 0; 522 + fputs (stbufline, stbuf); 523 + } 524 + 525 + free (stbufline); 526 + fclose (stbuf); 527 + 528 +acmpl_begin: 529 + 530 +// Run st-autocomplete 531 + 532 + sprintf ( 533 + acmpl, 534 + "cat %100s | st-autocomplete %500s %d %d", // ACMPL_ISSUE: why 100 and 500? 535 + stbuffile, 536 + acmpl_cmd [acmpl_cmdindex], 537 + cy, 538 + cx 539 + ); 540 + 541 + acmpl_exec = popen (acmpl, "r"); // ACMPL_ISSUE: popen isn't defined by The Standard. Does it work in BSDs for example? 542 + // check for popen error ... 543 + 544 +// Read the target, targetlen 545 + 546 + fscanf (acmpl_exec, "%500s\n", target); // check for scanning error ... 547 + targetlen = strlen (target); 548 + } 549 + 550 +// Read a completion if exists (acmpl_status) 551 + 552 + unsigned line, beg, end; 553 + 554 + acmpl_status = fscanf (acmpl_exec, "%500s %u %u %u\n", completion, & line, & beg, & end); 555 + // ACMPL_ISSUE: why 500? use term.col instead 556 + 557 +// Exit if no completions found 558 + 559 + if (active == 0 && acmpl_status == EOF) 560 + { 561 + 562 +// Close st-autocomplete and exit without activating the autocomplete mode 563 + 564 + pclose (acmpl_exec); 565 + remove (stbuffile); 566 + return; 567 + } 568 + 569 +// If completions found, enable autocomplete mode and autocomplete the target 570 + 571 + active = 1; 572 + 573 +// Clear target before first completion 574 + 575 + if (complen_prev == 0) 576 + { 577 + for (size_t i = 0; i < targetlen; i++) 578 + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is a right solution 579 + } 580 + 581 +// Clear previuos completion if this is not the first 582 + 583 + else 584 + { 585 + selclear (); 586 + for (size_t i = 0; i < complen_prev; i++) 587 + ttywrite ((char []) { '\b' }, 1, 1); // ACMPL_ISSUE: I'm not sure that this is a right solution 588 + complen_prev = 0; 589 + } 590 + 591 +// If no more completions found, reset and restart 592 + 593 + if (acmpl_status == EOF) 594 + { 595 + active = 0; 596 + pclose (acmpl_exec); 597 + ttywrite (target, targetlen, 0); 598 + goto acmpl_begin; 599 + } 600 + 601 +// Read the new completion and autcomplete 602 + 603 + selstart (beg, line, 0); 604 + selextend (end - 1, line, 1, 0); 605 + xsetsel (getsel ()); 606 + 607 + complen_prev = strlen (completion); 608 + ttywrite (completion, complen_prev, 0); 609 +} 610 diff -uraN st-0.8.4/st.h st-autocomplete/st.h 611 --- st-0.8.4/st.h 2021-12-18 10:56:10.545143280 +0400 612 +++ st-autocomplete/st.h 2021-12-18 10:56:10.545143280 +0400 613 @@ -77,6 +77,8 @@ 614 const char *s; 615 } Arg; 616 617 +void autocomplete (const Arg *); 618 + 619 void die(const char *, ...); 620 void redraw(void); 621 void draw(void); 622 diff -uraN st-0.8.4/x.c st-autocomplete/x.c 623 --- st-0.8.4/x.c 2021-12-18 10:56:10.545143280 +0400 624 +++ st-autocomplete/x.c 2021-12-18 10:56:10.545143280 +0400 625 @@ -1803,11 +1803,15 @@ 626 /* 1. shortcuts */ 627 for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) { 628 if (ksym == bp->keysym && match(bp->mod, e->state)) { 629 + if (bp -> func != autocomplete) 630 + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); 631 bp->func(&(bp->arg)); 632 return; 633 } 634 } 635 636 + autocomplete ((const Arg []) { ACMPL_DEACTIVATE }); 637 + 638 /* 2. custom keys from config.h */ 639 if ((customkey = kmap(ksym, e->state))) { 640 ttywrite(customkey, strlen(customkey), 1);