# tabkey.ce - Tab key completion script for EPIC5 # Copyright (c) 2002-2013 EPIC Software Labs # See the 'COPYRIGHT' file for more information. # # Version: 1.3.1 # # This script is based on the tabkey.ce script written by CrazyEddy and # distributed with the EPIC4 IRC client. The original design and all # of the really important code are his so he deserves most of the credit. # If not for his genius none of this would be possible. :-) # # This version is being extended and maintained by Brian Weiss. # Please send all bug reports, questions, and comments to brian@epicsol.org. # # This script uses serial number 420 for all /ON hooks. # # Original header follows, with modifications where necessary. # # -bmw # # # OK, here's the plan: # # tabkey.main cuts the input line up into edible chunks, and selects a function # to do the actual completion. The function will receive a single word as # its argument, and return a list of potential matches. The word is whatever # is under the cursor, up to the insertion point at the current time. The # word may be empty and the function should be able to cope with this. # # The exact function chosen works like this: # * If the current _word_ is a command (first word, begins with $cmdchars), # call command_completion by way of parsekey (don't call any aliases). # * Or, if the current line is a command, it gets handed to $tabkey.cmd(). # * Otherwise, call tabkey.default, which is a stub that can be changed # to suit your preferences. The default tabkey.default calls # tabkey.nickchan which matches against all channels you're currently on # and either the nicks in your current channel, or failing that, the # nicks in all channels. # * tabkey.default is also called if tabkey.cmd() returns nothing. If # necessary, the function can prevent this behaviour by returning a single # space, but this is discouraged. # * tabkey.default is also called by $tabkey.cmd() if it cannot find an # appropriate command handler. # * $tabkey.cmd() will search for an appropriate function by joining the # command and all its arguments together with dots and progressively # removing those arguments until a function is found. # # tabkey "exports" a number of local variables that the functions may use # or alter after using "bless": # * $curword is what tabkey believes the current word is. The function # may make use of this for context sensitivity. I say believes because # this may be open to different interpretations. # * $wordind is the character index of the _beginning_ of the word that is # being completed. If it is equal to $curpos(), then the first argument # will be empty and the user has hit tab at the beginning of the word or # in between words and the function should return all possible matches. # The reason you may need to use it is if the function is designed to # complete something that is not a single word. It, and the current # cursor position may be altered to cause tabkey to replace the chosen # part of the string instead of just the word. # # The organisation of the supporting functions goes like this: # * "Context sensitive" functions are to be named with "tabkey.cmd." as a # prefix followed by the name of the command that they are used to # complete. This is where tabkey.cmd will look for them. # * Generally, completion "methods" should be placed under tabkey.method # and shouldn't rely on the exported variables being available, as user # supplied functions may forget to re-export them. Context sensitive # functions should be as simple as possible. # # NOTE: The "first argument" description above is no longer true. The # functions argument list is to be treated as a single argument. # # Make sure this file gets loaded with the PF loader if (word(2 $loadinfo()) != [pf]) { load -pf $word(1 $loadinfo()); return; }; # # /SET vars # @ symbolctl(create TABKEY_COMPLETE_TOLOWER); @ symbolctl(set TABKEY_COMPLETE_TOLOWER 1 builtin_variable type bool); ^set TABKEY_COMPLETE_TOLOWER OFF; @ symbolctl(create TABKEY_MAX_MSG_HISTORY); @ symbolctl(set TABKEY_MAX_MSG_HISTORY 1 builtin_variable type int); ^set TABKEY_MAX_MSG_HISTORY 6; @ symbolctl(create TABKEY_MSGHIST_SENT_ONLY); @ symbolctl(set TABKEY_MSGHIST_SENT_ONLY 1 builtin_variable type bool); ^set TABKEY_MSGHIST_SENT_ONLY OFF; @ symbolctl(create TABKEY_NICKCOMP_SUFFIX); @ symbolctl(set TABKEY_NICKCOMP_SUFFIX 1 builtin_variable type str); ^set TABKEY_NICKCOMP_SUFFIX ,; # # The following functions are to be used with the /BIND command # to cause a particular key sequence to perform completion on the # current input line. # @ bindctl(function TABKEY_COMPLETION create "tabkey.main"); @ bindctl(function TABKEY_COMPLETION_FORWARD create "tabkey.main 1"); @ bindctl(function TABKEY_COMPLETION_BACKWARD create "tabkey.main -1"); # # This binds the Tab key to the functions that call tabkey.main. # By default this will enable forward match cycling. # bind ^I TABKEY_COMPLETION_FORWARD; bind ^[^I TABKEY_COMPLETION_BACKWARD; if (info(i) < 1000) { @ TABKEY.LEVELS = [ALL NONE CRAP PUBLIC MSGS NOTICES WALLS WALLOPS NOTES OPNOTES SNOTES ACTIONS DCC CTCP USERLOG1 USERLOG2 USERLOG3 USERLOG4]; } else { @ TABKEY.LEVELS = [ALL NONE CRAP PUBLICS MSGS NOTICES WALLS WALLOPS OPNOTES SNOTES ACTIONS DCCS CTCPS INVITES JOINS NICKS TOPICS PARTS QUITS KICKS MODES USER1 USER2 USER3 USER4 USER5 USER6 USER7 USER8 USER9 USER10]; }; # # tabkey.main [-1|0|1] # # This is the main alias that will be called by the bind functions # created with $bindctl() above. Match cycling can be enabled by # passing either '1' or '-1' as the first argument ('1' for forward # cycling, '-1' for backward cycling). If no argument (or '0') is # specified then matches will not be cycled. # alias tabkey.main (cycle, void) { @ :xd_extractw = xdebug(extractw); xdebug extractw; @ :curpos = curpos(); @ :curword = indextoword($curpos $L); @ :wordind = wordtoindex($curword $L); @ :cmdind = 0; if (32 >= (127 & ascii($mid(${curpos - 1} 1 $L)))) { @ curword++; @ wordind = curpos; }; @ :word = mid($wordind ${curpos-wordind} $L); # # Attempt to cycle through the list of matches if tabkey.main is # run a second time without any change in the input line. -bmw # if (cycle && #TABKEY.LAST_MATCHES && L == TABKEY.LAST_LINE) { # Adjust the index if we change directions. if (TABKEY.LAST_CYCLE_DIRECTION && cycle != TABKEY.LAST_CYCLE_DIRECTION) repeat 2 @ TABKEY.INDEX += cycle; if (cycle > 0 && (TABKEY.INDEX == #TABKEY.LAST_MATCHES || TABKEY.INDEX < 0)) { @ TABKEY.INDEX = 0; } else if (cycle < 0 && (TABKEY.INDEX == [] || TABKEY.INDEX < 0)) { @ TABKEY.INDEX = #TABKEY.LAST_MATCHES - 1; }; @ :nextword = word($TABKEY.INDEX $TABKEY.LAST_MATCHES); parsekey delete_to_previous_space; xtype -l $nextword; @ TABKEY.INDEX += cycle; @ TABKEY.LAST_LINE = L; @ TABKEY.LAST_CYCLE_DIRECTION = cycle; xdebug $xd_extractw; return; }; if (word =~ ["*"]) { @ :word = shift(word); } elsif (word =~ ["*]) { @ :word #= ["]; @ :word = shift(word); }; if (!index($cmdchars $L)) { wait for @ :matches = tabkey.cmd($word); } elsif (L == []) { wait for @ :matches = tabkey.cmd.msg($*); } else { wait for @ :matches = tabkey.default($word); }; @ :prefix = prefix($matches); if (#matches == 1 && matches != [ ]) { @ :prefix = 0 > index("$chr($jot(32 1))" $prefix) ? prefix : ["$prefix"]; @ :line = mid(0 $wordind $L); parsekey erase_to_beg_of_line; xtype -l $line$prefix${[ ]}; } else if (#matches > 1) { if (word == prefix) { echo Completions for \"$word\": $matches; @ TABKEY.INDEX = -1; @ TABKEY.LAST_LINE = L; @ TABKEY.LAST_MATCHES = matches; @ TABKEY.LAST_CYCLE_DIRECTION = []; } else if (strlen($word) <= strlen($prefix)) { @ :prefix = 0 > index("$chr($jot(32 1))" $prefix) ? prefix : ["$prefix"]; @ :line = mid(0 $wordind $L); parsekey erase_to_beg_of_line; xtype -l $line$prefix; }; }; xdebug $xd_extractw; }; alias tabkey.cmd (...) { bless; @ :ret = :cmd = []; @ :pass = chr($jot($ascii(AZ))); @ :pass #= chr($jot($ascii(az))); @ :pass #= chr($jot($ascii(09))); @ :args = wordtoindex($cmdind $L); @ :args = mid($args $curpos $L); # # Make absolutely sure we remove $CMDCHARS from the beginning of the # the string. This is needed whenever $CMDCHARS contains one of the # characters in the call to $pass(). -bmw # if (left($strlen($CMDCHARS) $args) == CMDCHARS) { @ :args = rest($strlen($CMDCHARS) $args); }; @ :args = unsplit(. $args); @ :args = pass(._$pass $args); if (!curword) { @ :ret = tabkey.method.command($args); repeat $#ret {push ret $K$shift(ret)}; return $ret; }; while (args) { @ :matches = aliasctl(alias pmatch tabkey.cmd.$args*); @ :matches = prefix($matches); if (curword == count(. $args)) { } elsif (matches != [tabkey.cmd.$args]) { } elsif (aliasctl(alias exists $matches)) { @ args = matches; break; }; @ args = before(-1 . $args); }; if (args) { wait for @ :ret = ${args}($*); }; unless (strlen($ret)) { wait for @ :ret = tabkey.default($*); }; return $ret; }; alias tabkey.default (...) { bless; if (curword == 0) { @ :matches = tabkey.method.nick($*); if (#matches == 1) { @ function_return = matches ## TABKEY_NICKCOMP_SUFFIX; } else { @ function_return = matches ? matches : tabkey.method.nickchannotify($*); }; } else { @ function_return = tabkey.method.nickchannotify($*); }; }; alias tabkey.msg_handler (serv, nick, void) { if (!nick) { echo Error: tabkey.msg_handler: Not enough arguments; return; }; if (match($nick $TABKEY.MSGHIST[$encode($serv)])) { @ TABKEY.MSGHIST.$encode($serv) = remw($nick $TABKEY.MSGHIST[$encode($serv)]); }; if (numwords($TABKEY.MSGHIST[$encode($serv)]) == TABKEY_MAX_MSG_HISTORY) { shift TABKEY.MSGHIST.$encode($serv); }; push TABKEY.MSGHIST.$encode($serv) $nick; @ TABKEY.MSGHIST_INDEX.$encode($serv) = -1; }; # # This prevents tabkey.main from falling back to tabkey.default if # the completion routine returns nothing. See aliases below # for examples. -bmw # alias tabkey.nofallback (...) { @ function_return = [$*] == [] ? [ ] : [$*]; }; # # This was contributed by zlonix # alias tabkey.normalize (input) { if (TABKEY_COMPLETE_TOLOWER == [ON]) { return $tolower($input); } else { return $input; }; }; # Used for things like /eval and /repeat where what you are completing # is another command. depth is the arg number where the command begins. # This is not well cpu optimised. See examples below. alias tabkey.recurse (depth, ...) { bless; @ :curpos += wordtoindex($cmdind $L); @ :curpos -= wordtoindex(${cmdind+depth} $L); @ :cmdind += depth; @ :curword -= depth; @ function_return = 0 > curword ? [] : tabkey.cmd($*); @ :curword += depth; @ :cmdind -= depth; @ :curpos += wordtoindex(${cmdind+depth} $L); @ :curpos -= wordtoindex($cmdind $L); }; # Context sensitive completion goes here. alias tabkey.cmd.alias (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(alias))); }; alias tabkey.cmd.assign (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(assign))); }; alias tabkey.cmd.channel (...) { @ function_return = tabkey.nofallback($tabkey.method.chan($*)); }; alias tabkey.cmd.dcc (args) { bless; if (curword == 1) { @ function_return = tabkey.normalize($tabkey.nofallback($pattern($args* CHAT CLOSE CLOSEALL GET LIST RAW RENAME RESUME SEND))); }; }; alias tabkey.cmd.dcc.resume (...) { tabkey.cmd.dcc.send $*; }; alias tabkey.cmd.dcc.send (...) { bless; switch ($curword) { (0) {echo This should never happen.;call;local} (1) {echo This should never happen.;call;local} (2) {@ function_return = tabkey.nofallback($tabkey.method.nick($*))} (*) {@ function_return = tabkey.nofallback($tabkey.method.file($*))} }; }; alias tabkey.cmd.do (void) { tabkey.recurse 1; }; alias tabkey.cmd.eval (void) { tabkey.recurse 1; }; alias tabkey.cmd.exec (...) { @ function_return = tabkey.nofallback($tabkey.method.file($*)); }; alias tabkey.cmd.foreach (...) { bless; if (curword == 1 && left(1 $word) == [-]) { @ :retval = tabkey.method.variable(alias); } else if (curword == 1) { @ :retval = tabkey.method.variable(assign); }; @ function_return = tabkey.nofallback($retval); }; alias tabkey.cmd.help (...) { bless; @ :matches = []; @ :path = restw(1 $left($curpos $L)); if (wordind == curpos) {@ push(path *)}; repeat $#path @ push(path $shift(path)*); @ matches = globi($unsplit(/ $getset(HELP_PATH) $path)); repeat $#matches @ push(matches $rightw(1 $remws(/ $split(/ $shift(matches))))); @ function_return = tabkey.nofallback($remw(CVS $matches)); }; alias tabkey.cmd.ig (...) { tabkey.cmd.ignore $*; }; alias tabkey.cmd.ignore (...) { bless; if (curword == 1) { @ :retval = tabkey.method.mask($*); } else if (curword > 1) { @ :retval = pattern($0* $TABKEY.LEVELS); }; @ function_return = tabkey.nofallback($retval); }; alias tabkey.cmd.j (...) { tabkey.cmd.join $*; }; alias tabkey.cmd.join (...) { @ function_return = tabkey.nofallback($tabkey.method.chan($*)); }; alias tabkey.cmd.l (...) { tabkey.cmd.leave $*; }; alias tabkey.cmd.leave (...) { @ function_return = tabkey.nofallback($tabkey.method.chan($*)); }; alias tabkey.cmd.less (...) { @ function_return = tabkey.nofallback($tabkey.method.file($*)); }; alias tabkey.cmd.load (...) { @ function_return = tabkey.nofallback($tabkey.method.file($*)); }; alias tabkey.cmd.mode (...) { bless; if (curword == 1) { @ :retval = tabkey.method.chan($*); } else { @ :retval = tabkey.method.nick($*); }; @ function_return = tabkey.nofallback($retval); }; alias tabkey.cmd.m (...) { tabkey.cmd.msg $*; }; alias tabkey.cmd.msg (...) { bless; if (curword == 1) { @ :retval = pattern($word* $sort($uniq($TABKEY.MSGHIST[$encode($winserv())] $tabkey.method.nickchan($*)))); }{ @ :cmd = word(0 $L); if (!cmd) { @ :cmd = [$K\MSG]; }; @ :cmd = tabkey.normalize($cmd); @ :nicks = TABKEY.MSGHIST[$encode($winserv())]; @ :index = TABKEY.MSGHIST_INDEX[$encode($winserv())]; @ :trailing = restw(2 $L); if (nicks) { if (index > 0) { @ :nick = word(${index - 1} $nicks); @ TABKEY.MSGHIST_INDEX.$encode($winserv()) = index - 1; }{ @ :nick = word(${numwords($nicks) - 1} $nicks); @ TABKEY.MSGHIST_INDEX.$encode($winserv()) = numwords($nicks) - 1; }; parsekey erase_line; xtype -l $cmd $nick $trailing; @ :retval = [ ]; }{ @ TABKEY.MSGHIST_INDEX.$encode($winserv()) = -1; }; }; @ function_return = retval; }; alias tabkey.cmd.part (...) { tabkey.cmd.leave $*; }; alias tabkey.cmd.pop (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(assign)); }; alias tabkey.cmd.push (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(assign)); }; alias tabkey.cmd.q (...) { tabkey.cmd.query $*; }; alias tabkey.cmd.query (...) { @ function_return = pattern($0* $sort($uniq($TABKEY.MSGHIST[$encode($winserv())] $tabkey.method.nickchan($*)))); }; alias tabkey.cmd.repeat (void) { tabkey.recurse 2; }; alias tabkey.cmd.set (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(set)); }; alias tabkey.cmd.shift (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(assign)); }; alias tabkey.cmd.unshift (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(assign)); }; alias tabkey.cmd.whois (...) { @ function_return = tabkey.nofallback($tabkey.method.nick($*)); }; alias tabkey.cmd.window (...) { bless; @ :windowcmds = [ADD BACK BALANCE BEEP_ALWAYS BIND CHANNEL CLEAR CREATE DELETE DESCRIBE DISCON DOUBLE ECHO FIXED FLUSH GOTO GROW HIDE HIDE_OTHERS HOLD_INTERVAL HOLD_MODE KILL KILL_OTHERS KILLSWAP LAST LASTLOG LASTLOG_LEVEL LEVEL LIST LOG LOGFILE MOVE NAME NEW NEW_HIDE NEXT NOSERV NOTIFIED NOTIFY NOTIFY_LEVEL NUMBER POP PREVIOUS PROMPT PUSH QUERY REFNUM REFNUM_OR_SWAP REFRESH REMOVE SCRATCH SCROLL SCROLLBACK SEARCH_BACK SEARCH_FORWARD SERVER SHOW SHOW_ALL SHRINK SIZE SKIP STACK STATUS_FORMAT STATUS_FORMAT1 STATUS_FORMAT2 STATUS_SPECIAL SWAP UNBIND]; if (curword == 1) { @ :cmd = [WINDOW]; } else { @ :cmd = word(${curword - 1} $L); }; switch ($cmd) { (ADD) (QUERY) (REMOVE) (SEARCH_BACK) (SEARCH_FORWARD) { @ :list = tabkey.method.nickchan($*); } (BEEP_ALWAYS) (DOUBLE) (FIXED) (HOLD_MODE) (LOG) (SCRATCH) (SCROLL) (SKIP) { @ :list = [ON OFF TOGGLE]; } (BIND) (CHANNEL) { @ :list = tabkey.method.chan($*); } (ECHO) (GOTO) (GROW) (HOLD_INTERVAL) (LASTLOG) (MOVE) (NAME) (NUMBER) (PROMPT) (REFNUM) (REFNUM_OR_SWAP) (SCROLLBACK) (SERVER) (SHRINK) (SIZE) (STATUS_%) (SWAP) { @ :nocomp = 1; } (LASTLOG_LEVEL) (LEVEL) (NOTIFY_LEVEL) { if (match(%,% $word)) { @ :word_a = before(-1 , $word); @ :word_b = after(-1 , $word); }; @ :list = TABKEY.LEVELS; } (LOGFILE) { @ :list = tabkey.method.file($*); } (*) { @ :list = windowcmds; } }; if (word_a) { @ :matches = pattern($word_b* $list); if (#matches == 1) { parsekey delete_to_previous_space; xtype -l $word_a\,$matches${[ ]}; } else if (#matches > 1) { parsekey delete_to_previous_space; xtype -l $word_a\,$prefix($matches); echo Completions for \"$word_b\": $matches; }; @ :retval = [ ]; } else if (nocomp) { @ :retval = [ ]; } else { @ :retval = pattern($word* $list); }; @ function_return = tabkey.normalize($tabkey.nofallback($retval)); }; # Methods. alias tabkey.method.array (array, args) { for ii from 0 to ${numitems($array) - 1} { @ push(:list $getitem($array $ii)); }; @ :list = sort($uniq($list)); @ function_return = args ? pattern($args* $list) : list; }; alias tabkey.method.command (...) { @ function_return = tabkey.normalize($sort($uniq($getcommands($**) $aliasctl(alias match $*) $aliasctl(alias match $**)))); }; alias tabkey.method.chan (...) { @ function_return = (:foo = pattern($sar(g/\\/\\\\/$*)* $mychannels())) ? foo : (:foo = pattern(#$sar(g/\\/\\\\/$*)* $mychannels())) ? foo : pattern(&$sar(g/\\/\\\\/$*)* $mychannels()); }; stack push alias alias.foo; alias alias.foo (...) {alias $*;alias $sar(g/globi/glob/$sar(g/filei/file/$*))}; alias.foo tabkey.method.filei (f0) { @ :f0 = sar(gr/\\/\\\\/f0); @ :f0 = sar(gr/*/\\*/f0); @ :f0 = sar(gr/?/\\?/f0); @ :f0 = sar(gr/[/\\[/f0); @ :f0 = sar(gr/]/\\]/f0); @ :f0 = :f1 = globi("" $f0*); while (numwords($f0) == 1 && f0 =~ [*/]) { @ :f0 = globi("" $f0*); }; @ function_return = f0 ? f0 : f1; }; stack pop alias alias.foo; alias tabkey.method.mask (...) { @ :uhost = userhost($0); if (uhost == [@] && aliasctl(alias exists uh)) { @ :uhost = uh($0); }; @ function_return = uhost == [@] ? [] : mask(3 $uhost); }; alias tabkey.method.nick (...) { @ :args = sar(g/\\/\\\\/$*); if (match($args* $chanusers())) { @ function_return = pattern($args* $chanusers()); } else { @ :chanusers = mychannels(); repeat $#chanusers @ push(chanusers $chanusers($shift(chanusers))); @ function_return = pattern($args* $sort($uniq($chanusers))); }; }; alias tabkey.method.nickchan (...) { @ :chans = pattern($sar(g/\\/\\\\/$*)* $mychannels()); @ function_return = remws(/ $chans $tabkey.method.nick($*)); }; alias tabkey.method.nickchannotify (...) { @ :chans = pattern($sar(g/\\/\\\\/$*)* $mychannels()); @ function_return = remws(/ $chans $sort($uniq($tabkey.method.nick($*) $tabkey.method.notify($*)))); }; alias tabkey.method.notify (...) { @ function_return = pattern($sar(g/\\/\\\\/$*)* $notify(on)); }; alias tabkey.method.variable (type, void) { bless; if (!type) return; if (curword == 1) { if (left(1 $word) == [-]) { @ :rm = 1; @ :word2 = rest(1 $word); } else { @ :word2 = word; }; switch ($type) { (ALIAS) (ASSIGN) {@ :matches = aliasctl($type match $word2);} (SET) {@ :matches = getsets($word2*);} }; if (rm) { for ii in ($matches) { @ push(:retval -$ii); }; } else { @ :retval = matches; }; } else if (curword == 2 && word == [] && (left(1 $word(${curword - 1} $L)) != [-])) { eval xtype -l \$$word(${curword - 1} $L); }; @ function_return = tabkey.normalize($retval); }; # # Add the nicks of users that you're chatting with to the msg history. # on #^msg 420 "*" { if (TABKEY_MSGHIST_SENT_ONLY == [OFF]) { tabkey.msg_handler $servernum() $0; }; }; on #^send_msg 420 "*" { tabkey.msg_handler $servernum() $0; }; on #^dcc_chat 420 "*" { if (TABKEY_MSGHIST_SENT_ONLY == [OFF]) { for serv in (-1 $myservers(0)) { tabkey.msg_handler $serv =$0; }; }; }; on #^send_dcc_chat 420 "*" { for serv in (-1 $myservers(0)) { tabkey.msg_handler $serv =$0; }; }; on #^dcc_request 420 "% CHAT *" { if (TABKEY_MSGHIST_SENT_ONLY == [OFF]) { for serv in (-1 $myservers(0)) { tabkey.msg_handler $serv =$0; }; }; }; on #^send_ctcp 420 "PRIVMSG % DCC CHAT *" { for serv in (-1 $myservers(0)) { tabkey.msg_handler $serv =$1; }; };