["\'`])|(?\[)|(?\())'; 111 | 112 | $matcher = generate_matcher('.*?'); 113 | 114 | # Here we match text till enclosing pair, using perl conditionals in 115 | # regexps (?(condition)yes-expression|no-expression). 116 | # \0 is used to hack concatenation with '*' later in the code. 117 | $char_class_at_end = '.*?(.(?=(?()\]|((?(
)\)|\g{q})))))\0'; 118 | $char_class_to_complete = '\S'; 119 | } 120 | 121 | 122 | my ($row, $col) = $self->screen_cur(); # get cursor coordinates 123 | 124 | # use the last used word or read the word behind the cursor 125 | my $word_to_complete = $self->{last_word} // 126 | read_word_at_coord($self, $row, $col, 127 | $char_class_to_complete); 128 | 129 | if ($word_to_complete) { 130 | # ignore the completed word itself 131 | $self->{already_completed}{$word_to_complete} = 1; 132 | 133 | # continue the last search or start from the current row 134 | my $completion = find_match($self, 135 | $word_to_complete, 136 | $self->{next_row} // $row, 137 | $matcher->($word_to_complete), 138 | $char_class_before, 139 | $char_class_at_end); 140 | if ($completion) { 141 | # save the last completed word unless continuing the last search 142 | $self->{last_word} //= $word_to_complete; 143 | 144 | replace_text($self, 145 | $self->{last_completion} // $word_to_complete, 146 | $completion); 147 | $self->{last_completion} = $completion; 148 | 149 | highlight_match($self, 150 | $self->{next_row}+1, 151 | $completion); 152 | } 153 | } 154 | return 1; 155 | } 156 | 157 | leave($self); 158 | 159 | () 160 | } 161 | 162 | ###################################################################### 163 | 164 | sub conditional_leave { 165 | my ($self, $event, $keysym) = @_; 166 | 167 | my $keyname = $self->XKeysymToString($keysym); 168 | $_ = $keyname; 169 | 170 | if ($keyname eq "Escape") { 171 | undo_completion($self); 172 | $self->disable("key_press"); 173 | return 1; 174 | } 175 | 176 | my $ShiftMask = (1<<0); 177 | my $ControlMask = (1<<2); 178 | my $Mod1Mask = (1<<3); 179 | my $Mod4Mask = (1<<6); 180 | my $Mod5Mask = (1<<7); 181 | 182 | my $modifier_mask = $ControlMask 183 | | $Mod1Mask # Alt 184 | | $Mod5Mask # AltGr 185 | | $Mod4Mask; # Super 186 | 187 | # Stop the completion mode only when either a alphanumeric key is 188 | # pressed (regardless of any modifiers) or a printable key is 189 | # pressed without any modifiers. It is needed because the 190 | # key_press hooks are processed before the user_command hooks and 191 | # stopping on every printable character would break the completion 192 | # chaining. For this to work, the actions must be bound to the 193 | # symbol characters with some modifiers (for example 194 | # Ctrl+Alt+slash). 195 | if (/^\w$/ || 196 | (!($event->{state} & $modifier_mask) && 197 | ($keysym <= 127 || 198 | $keyname eq "Return" || 199 | $keyname eq "BackSpace" || 200 | $keyname eq "Delete" || 201 | $keyname eq "Tab" || 202 | $keyname eq "Up" || 203 | $keyname eq "Down" || 204 | $keyname eq "Left" || 205 | $keyname eq "Right"))) { 206 | 207 | leave($self); 208 | $self->disable("key_press"); 209 | } 210 | 211 | () 212 | } 213 | 214 | ###################################################################### 215 | 216 | sub highlight_match { 217 | my ($self, $linenum, $completion) = @_; 218 | 219 | clear_highlight($self); 220 | 221 | my $line = $self->line($linenum); 222 | my $re = quotemeta $completion; 223 | 224 | $line->t =~ /$re/; 225 | 226 | my ($beg_row, $beg_col) = $line->coord_of($-[0]); 227 | my ($end_row, $end_col) = $line->coord_of($+[0]); 228 | 229 | $self->enable(refresh_begin => sub { 230 | if (exists $self->{highlight}) { 231 | clear_highlight($self); 232 | } 233 | $self->{highlight} = [$beg_row, $beg_col, $end_row, $end_col]; 234 | $self->scr_xor_span(@{$self->{highlight}}, urxvt::RS_RVid); 235 | $self->{pty_ev_events} = $self->pty_ev_events(urxvt::EV_NONE); 236 | }, 237 | key_press => \&conditional_leave); 238 | } 239 | 240 | ###################################################################### 241 | 242 | sub clear_highlight { 243 | my $self = shift; 244 | 245 | if (exists $self->{highlight}) { 246 | $self->scr_xor_span(@{$self->{highlight}}, urxvt::RS_RVid); 247 | $self->pty_ev_events($self->{pty_ev_events}); 248 | delete $self->{pty_ev_events}; 249 | 250 | delete $self->{highlight}; 251 | $self->disable("refresh_begin"); 252 | } 253 | () 254 | } 255 | 256 | ###################################################################### 257 | 258 | sub replace_text { 259 | my ($self, $current_text, $replacement) = @_; 260 | 261 | # "press" backspace to erase the text; probably not portable 262 | $self->tt_write($self->locale_encode(chr(0x7F) x length $current_text)); 263 | 264 | # print out the replacement 265 | $self->tt_write($self->locale_encode($replacement)); 266 | } 267 | 268 | ###################################################################### 269 | 270 | sub read_word_at_coord { 271 | my ($self, $row, $col, $char_class) = @_; 272 | 273 | $_ = substr($self->ROW_t($row), 0, $col); # get the current line up to the cursor... 274 | s/.*?($char_class*)$/$1/; # ...and read the last word from it 275 | return $_; 276 | } 277 | 278 | ###################################################################### 279 | 280 | # Returns a function that takes a string and returns that string with 281 | # this function's argument inserted between its every two characters. 282 | # The resulting string is used as a regular expression matching the 283 | # completion candidates. 284 | sub generate_matcher { 285 | my $regex_between = shift; 286 | 287 | sub { 288 | $_ = shift; 289 | 290 | # sorry for this lispy code, I couldn't resist ;) 291 | (join "$regex_between", 292 | (map quotemeta, 293 | (split //))) 294 | } 295 | } 296 | 297 | ###################################################################### 298 | 299 | # Checks whether the completion found by find_match() was already 300 | # found and if it was, calls find_match() again to find the next 301 | # completion. 302 | # 303 | # Takes all the arguments that find_match() would take, to make a 304 | # mutually recursive call. 305 | sub skip_duplicates { 306 | my $self = $_[0]; 307 | my $completion = shift @{$self->{matches_in_row}}; # get the rightmost one 308 | 309 | # check for duplicates 310 | if (exists $self->{already_completed}{$completion}) { 311 | # skip this completion 312 | return find_match(@_); 313 | } else { 314 | $self->{already_completed}{$completion} = 1; 315 | return $completion; 316 | } 317 | } 318 | 319 | ###################################################################### 320 | 321 | # Finds the next matching completion in the row current row or above 322 | # while skipping duplicates using skip_duplicates(). 323 | sub find_match { 324 | my ($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end) = @_; 325 | $self->{matches_in_row} //= []; 326 | 327 | # cycle through all the matches in the current row if not starting a new search 328 | if (@{$self->{matches_in_row}}) { 329 | return skip_duplicates($self, $word_to_match, $current_row, $regexp, $char_class_before, $char_class_at_end); 330 | } 331 | 332 | 333 | my $i; 334 | # search through all the rows starting with current one or one above the last checked 335 | for ($i = $current_row; $i >= 0; --$i) { 336 | my $line = $self->line($i)->t; # get the line of text from the row 337 | 338 | my ($cursor_row, $cursor_column) = $self->screen_cur(); 339 | if ($i == $cursor_row) { 340 | $line = substr $line, 0, $cursor_column; 341 | } 342 | 343 | $_ = $line; 344 | 345 | # find all the matches in the current line 346 | my $match; 347 | push @{$self->{matches_in_row}}, $+{match} while ($_, $match) = / 348 | (.*${char_class_before}) 349 | (?
350 | ${regexp} 351 | ${char_class_at_end}* 352 | ) 353 | /ix; 354 | # corner case: match at the very beginning of line 355 | push @{$self->{matches_in_row}}, $+{match} if $line =~ /^(${char_class_before}){0}(? $regexp$char_class_at_end*)/i; 356 | 357 | if (@{$self->{matches_in_row}}) { 358 | # remember which row should be searched next 359 | $self->{next_row} = --$i; 360 | 361 | # arguments needed for find_match() mutual recursion 362 | return skip_duplicates($self, $word_to_match, $i, $regexp, $char_class_before, $char_class_at_end); 363 | } 364 | } 365 | 366 | # no more possible completions, revert to the original word 367 | undo_completion($self) if $i < 0; 368 | 369 | return undef; 370 | } 371 | 372 | ###################################################################### 373 | 374 | sub leave { 375 | my ($self) = @_; 376 | 377 | delete $self->{last_word}; 378 | delete $self->{last_completion}; 379 | delete $self->{next_row}; 380 | delete $self->{matches_in_row}; 381 | delete $self->{already_completed}; 382 | clear_highlight($self); 383 | } 384 | 385 | ###################################################################### 386 | 387 | sub undo_completion { 388 | my ($self) = @_; 389 | 390 | replace_text($self, $self->{last_completion}, $self->{last_word}); 391 | leave($self); 392 | } 393 | --------------------------------------------------------------------------------