5 use File
::Basename
qw(fileparse);
7 # if the last time you addressed someone was greater than this many minutes
9 # this avoids having people you have talked to a long time ago coming up too
10 # early in the completion list
11 # Setting this to 0 will disable the check which is effectively the same as
12 # setting it to infinity
13 my $last_use_threshold = 10; # 10 minutes
15 # added to the front of a completion the same way as a suffix, only if
16 # the word is at the beginning of the line
19 # ignore leading non-alphanumeric characters: -[\]^_`{|}
20 # Assuming you have the following nicks in a channel:
21 # [SomeNick] _SomeNick_ `SomeNick SomeNick SomeOtherNick
22 # when $ignore_leading_non_alnum is set to 0
23 # s<tab> will cycle through SomeNick and SomeOtherNick
24 # when $ignore_leading_non_alnum is set to 1
25 # s<tab> will cycle through [SomeNick] _SomeNick_ `SomeNick SomeNick
27 my $ignore_leading_non_alnum = 0;
29 # enable path completion
30 my $path_completion = 1;
33 # ignore the completion_amount setting and always cycle through nicks with tab
37 "Tab Completion", "1.0500", "Alternative tab completion behavior"
39 Xchat
::hook_print
( "Key Press", \
&complete
);
40 Xchat
::hook_print
( "Close Context", \
&close_context
);
41 Xchat
::hook_print
( "Focus Tab", \
&focus_tab
);
42 Xchat
::hook_print
( "Part", \
&clean_selected
);
43 Xchat
::hook_print
( "Part with Reason", \
&clean_selected
);
44 Xchat
::hook_command
( "", \
&track_selected
);
51 sub LEFT_TAB
() { 0xFE20 }
74 my $escapes = join "", keys %escape_map;
75 $escapes = qr/[\Q$escapes\E]/;
77 # used to determine if a word is the start of a path
78 my $path_pattern = qr{^(?:~|/|[[:alpha:]]:\\)};
81 my ($key, $modifiers) = @
{$_[0]};
82 # if $_[0][0] contains the value of the key pressed
83 # $_[0][1] contains modifiers
84 # the value for tab is 0xFF09
85 # the value for shift-tab(Left Tab) is 0xFE20
86 # we don't care about other keys
88 # the key must be a tab and left tab
89 return Xchat
::EAT_NONE
unless $key == TAB
|| $key == LEFT_TAB
;
91 # if it is a tab then it must not have any modifiers
92 return Xchat
::EAT_NONE
if $key == TAB
&& $modifiers & (CTRL
|ALT
|SHIFT
);
94 # loop backwards for shift+tab/left tab
95 my $delta = $modifiers & SHIFT ?
-1 : 1;
96 my $context = Xchat
::get_context
;
97 $completions{$context} ||= {};
99 my $completions = $completions{$context};
100 $completions->{pos} ||= -1;
102 my $suffix = Xchat
::get_prefs
( "completion_suffix" );
105 my $input = Xchat
::get_info
( "inputbox" );
106 my $cursor_pos = Xchat
::get_info
( "state_cursor" );
107 my $left = substr( $input, 0, $cursor_pos );
108 my $right = substr( $input, $cursor_pos );
109 my $length = length $left;
111 # trim spaces from the end of $left to avoid grabbing the wrong word
112 # this is mainly needed for completion at the very beginning where a space
113 # is added after the completion
116 # always add one to the index because
117 # 1) if a space is found we want the position after it
118 # 2) if a space isn't found then we get back -1
119 my $word_start = rindex( $left, " " ) + 1;
120 my $word = substr( $left, $word_start );
121 $left = substr( $left, 0, -length $word );
123 if( $cursor_pos == $completions->{pos} ) {
124 my $previous_word = $completions->{completed
};
125 my $new_left = $input;
126 substr( $new_left, $cursor_pos ) = "";
128 if( $previous_word and $new_left =~ s/(\Q$previous_word\E)$// ) {
130 $word_start = length( $new_left );
135 my $command_char = Xchat
::get_prefs
( "input_command_char" );
137 if( ($word !~ m{^[${command_char}]})
138 or ( $word =~ m{^[${command_char}]} and $word_start != 0 ) ) {
140 if( $cursor_pos == length $input # end of input box
141 # not a valid nick char
142 && $input =~ /(?<![\x41-\x5A\x61-\x7A\x30-\x39\x5B-\x60\x7B-\x7D-])$/
143 && $cursor_pos != $completions->{pos} # not continuing a completion
144 && $word !~ m{^(?:[&#/~]|[[:alpha:]]:\\)} # not a channel or path
146 # check for path completion
147 unless( $path_completion and $word =~ $path_pattern ) {
148 $word_start = $cursor_pos;
150 $length = length $length;
156 if( $word_start == 0 && $prefix && $word =~ /^\Q$prefix/ ) {
157 $word =~ s/^\Q$prefix//;
160 my $completed; # this is going to be the "completed" word
162 # for parital completions and channel names so a : isn't added
163 #$completions->{skip_suffix} = ($word =~ /^[&#]/) ? 1 : 0;
165 # continuing from a previous completion
167 exists $completions->{matches
} && @
{$completions->{matches
}}
168 && $cursor_pos == $completions->{pos}
169 && $word =~ /^\Q$completions->{matches}[$completions->{index}]/
171 $completions->{index} += $delta;
173 if( $completions->{index} < 0 ) {
174 $completions->{index} += @
{$completions->{matches
}};
176 $completions->{index} %= @
{$completions->{matches
}};
181 if( $word =~ /^[&#]/ ) {
182 # channel name completion
183 $completions->{matches
} = [ matching_channels
( $word ) ];
184 $completions->{skip_suffix
} = 0;
185 } elsif( $path_completion and $word =~ $path_pattern ) {
186 # file name completion
187 $completions->{matches
} = [ matching_files
( $word ) ];
188 $completions->{skip_suffix
} = 1;
191 # fix $word so { equals [, ] equals }, \ equals |
192 # and escape regex metacharacters
193 $word =~ s/($escapes)/$escape_map{$1}/g;
195 $completions->{matches
} = [ matching_nicks
( $word ) ];
196 $completions->{skip_suffix
} = 0;
198 $completions->{index} = 0;
201 $completed = $completions->{matches
}[ $completions->{index} ];
202 $completions->{completed
} = $completed;
204 my $completion_amount = Xchat
::get_prefs
( "completion_amount" );
206 # don't cycle if the number of possible completions is greater than
210 @
{$completions->{matches
}} > $completion_amount
211 && @
{$completions->{matches
}} != 1 )
213 # don't print if we tabbed in the beginning and the list of possible
214 # completions includes all nicks in the channel
215 my $context_type = Xchat
::context_info
->{type
};
216 if( $context_type != 2 # not a channel
217 or @
{$completions->{matches
}} < Xchat
::get_list
("users")
219 Xchat
::print( join " ", @
{$completions->{matches
}}, "\n" );
222 $completed = lcs
( $completions->{matches
} );
223 $completions->{skip_suffix
} = 1;
228 if( $word_start == 0 && !$completions->{skip_suffix
} ) {
229 # at the start of the line append completion suffix
230 Xchat
::command
( "settext $prefix$completed$suffix$right");
231 $completions->{pos} = length( "$prefix$completed$suffix" );
233 Xchat
::command
( "settext $left$completed$right" );
234 $completions->{pos} = length( "$left$completed" );
237 Xchat
::command
( "setcursor $completions->{pos}" );
243 my $input_length = length $input;
246 qq{input_length[$input_length]},
247 qq{cursor[$cursor_pos]},
248 qq{start[$word_start]},
251 qq{word[$word]}, qq{right[$right]},
252 qq{completed[}. ($completed||""). qq{]},
253 qq{pos[$completions->{pos}]},
256 local $Data::Dumper::Indent = 0;
257 Xchat::print Dumper $completions->{matches};
260 return Xchat
::EAT_ALL
;
262 return Xchat
::EAT_NONE
;
267 # all channels starting with $word
268 sub matching_channels
{
271 # for use in compare_channels();
273 local $current_chan = Xchat
::get_info
( "channel" );
275 my $conn_id = Xchat
::get_info
( "id" );
279 map { $_->[1]->{channel
} }
280 sort compare_channels
map {
281 my $chan = $_->{channel
};
284 # comparisons will be done based only on the name
285 # matching name, same connection, only channels
286 $chan =~ /^$word/i && $_->{id
} == $conn_id ?
293 return grep { $_->{type
} == 2 } Xchat
::get_list
( "channels" );
296 sub compare_channels
{
297 # package variable, value set in matching_channels()
300 # turn off warnings generated from channels that have not yet been visited
301 # since the script was loaded
302 no warnings
"uninitialized";
304 # the current channel is always first, then ordered by most recently visited
306 $a->[1]{channel
} eq $current_chan ?
-1 :
307 $b->[1]{channel
} eq $current_chan ?
1 :
308 $last_visit{ $b->[1]{context
} } <=> $last_visit{ $a->[1]{context
} }
309 || $a->[1]{channel
} cmp $b->[1]{channel
};
316 # for use in compare_nicks()
317 our ($my_nick, $selections, $now);
318 local $my_nick = Xchat
::get_info
( "nick" );
319 local $selections = $selected{ Xchat
::get_context
() };
322 my $pattern = $ignore_leading_non_alnum ?
323 qr/^[\-\[\]^_`{|}\\]*$word_re/i : qr/^$word_re/i;
326 sort compare_nicks
grep {
327 $_->{nick
} =~ $pattern;
328 } Xchat
::get_list
( "users" )
336 $max = $_ if $_ > $max;
342 # package variables set in matching_nicks()
346 for my $nick ( $a->{nick
}, $b->{nick
} ) {
347 # turn off the warnings that get generated from users who have yet
348 # to speak since the script was loaded
349 no warnings
"uninitialized";
351 if( $last_use_threshold
352 && (( $now - $selections->{$nick}) > ($last_use_threshold * 60)) ) {
353 delete $selections->{ $nick }
356 my $a_time = $selections->{ $a->{nick
} } || 0 ;
357 my $b_time = $selections->{ $b->{nick
} } || 0 ;
359 if( $a_time || $b_time ) {
360 return $b_time <=> $a_time;
361 } elsif( !$a_time && !$b_time ) {
362 return $b->{lasttalk
} <=> $a->{lasttalk
};
368 # more package variables, value set in matching_nicks()
371 # our own nick is always last, then ordered by the people we spoke to most
372 # recently and the people who were speaking most recently
374 $a->{nick
} eq $my_nick ?
1 :
375 $b->{nick
} eq $my_nick ?
-1 :
377 || Xchat
::nickcmp
( $a->{nick
}, $b->{nick
} );
379 # $selections->{ $b->{nick} } <=> $selections->{ $a->{nick} }
380 # || $b->{lasttalk} <=> $a->{lasttalk}
387 my ($file, $input_dir) = fileparse
( $word );
389 my $dir = expand_tilde
( $input_dir );
391 if( opendir my $dir_handle, $dir ) {
397 /^\Q$file/ } readdir $dir_handle;
399 @files = readdir $dir_handle;
403 File
::Spec
->catfile( $input_dir, $_ );
405 grep { !/^[.]{1,2}$/ } @files;
411 # Remove completion related data for tabs that are closed
413 my $context = Xchat
::get_context
;
414 delete $completions{$context};
415 delete $last_visit{$context};
416 return Xchat
::EAT_NONE
;
421 $last_visit{Xchat
::get_context
()} = time();
422 return Xchat
::EAT_NONE
;
425 # keep track of the last time a message was addressed to someone
426 # a message is considered addressed to someone if their nick is used followed
427 # by the completion suffix
429 my $input = $_[1][0];
430 return Xchat
::EAT_NONE
unless defined $input;
432 my $suffix = Xchat
::get_prefs
( "completion_suffix" );
433 for( grep defined, $input =~ /^(.+)\Q$suffix/, $_[0][0] ) {
434 if( in_channel
( $_ ) ) {
435 $selected{Xchat
::get_context
()}{$_} = time();
440 return Xchat
::EAT_NONE
;
443 # if a user is in the current channel
444 # user_info() can also be used instead of the loop
447 for my $nick ( nicks
() ) {
448 if( $nick eq $target ) {
456 # list of nicks in the current channel
458 return map { $_->{nick
} } Xchat
::get_list
( "users" );
461 # remove people from the selected list when they leave the channel
463 delete $selected{ Xchat
::get_context
() }{$_[0][0]};
464 return Xchat
::EAT_NONE
;
467 # Longest common substring
468 # Used for partial completion when using non-cycling completion
470 my @nicks = @
{+shift};
471 return "" if @nicks == 0;
472 return $nicks[0] if @nicks == 1;
474 my $substring = shift @nicks;
477 $substring = common_string
( $substring, shift @nicks );
484 my ($nick1, $nick2) = @_;
488 ($index < length $nick1) && ($index < length $nick2) &&
489 lc(substr( $nick1, $index, 1 )) eq lc(substr( $nick2, $index, 1 ))
493 return substr( $nick1, 0, $index );
499 $file =~ s/^~/home_dir()/e;
504 return $base_path if $base_path;
506 if ( $^O
eq "MSWin32" ) {
507 return $ENV{USERPROFILE
};
509 return ((getpwuid($>))[7] || $ENV{HOME
} || $ENV{LOGDIR
});