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