# version:1.2.2 loader:pf # $Id: tabkey.dsm,v 2.58 2011/02/22 19:25:16 brian Exp $ # # tabkey.dsm - Tab key completion module for DarkStar/EPIC5 # Copyright (c) 2002-2011 EPIC Software Labs # See the 'COPYRIGHT' file for more information. # # 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. # queue cleanup.tabkey { ^bind ^I NOTHING; ^bind ^[^I NOTHING; ^on #msg 420 -"*"; ^on #send_msg 420 -"*"; ^on #dcc_chat 420 -"*"; ^on #send_dcc_chat 420 -"*"; ^on #dcc_request 420 -"% CHAT *"; ^on #send_ctcp 420 -"PRIVMSG % DCC CHAT *"; }; addconfig TABKEY_MAX_MSG_HISTORY 6; addconfig -b TABKEY_MSGHIST_SENT_ONLY 0; addconfig 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"); bind ^I TABKEY_COMPLETION_FORWARD; bind ^[^I TABKEY_COMPLETION_BACKWARD; # # 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.commands($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 ## CONFIG.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)]) == CONFIG.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 = [$*] == [] ? [ ] : [$*]; }; # 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.autoload (...) { @ function_return = tabkey.nofallback($tabkey.method.array(_modules $*)); }; alias tabkey.cmd.channel (...) { @ function_return = tabkey.nofallback($tabkey.method.chan($*)); }; alias tabkey.cmd.config (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(config)); }; alias tabkey.cmd.dcc (args) { bless; if (curword == 1) { @ function_return = 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.dhelp (...) { @ :foo = HELP_PATH; ^set HELP_PATH $DS.HELP_DIR; wait for @ :retval = tabkey.cmd.help($*); ^set HELP_PATH $foo; @ function_return = tabkey.nofallback($retval); }; alias tabkey.cmd.do (void) { tabkey.recurse 1; }; alias tabkey.cmd.dset (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(config)); }; 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.fset (...) { @ function_return = tabkey.nofallback($tabkey.method.variable(format)); }; 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* $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.loadmod (...) { @ function_return = tabkey.nofallback($tabkey.method.array(_modules $*)); }; 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]; }; @ :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.reloadmod (...) { @ function_return = tabkey.nofallback($tabkey.method.array(_loaded_modules $*)); }; alias tabkey.cmd.repeat (void) { tabkey.recurse 2; }; alias tabkey.cmd.save (...) { @ function_return = tabkey.nofallback($pattern($** core) $tabkey.method.array(_loaded_modules $*)); }; 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.unloadmod (...) { @ function_return = tabkey.nofallback($tabkey.method.array(_loaded_modules $*)); }; 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.wi (...) { @ 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 = 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.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.commands (...) { @ function_return = 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)} (CONFIG) {@ :matches = getdsets($word2*)} (FORMAT) {@ :matches = getfsets($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)) != [-])) { switch ($type) { (CONFIG) (FORMAT) { eval xtype -l \$$type\.$word(${curword - 1} $L); }; (*) { eval xtype -l \$$word(${curword - 1} $L); }; }; }; @ function_return = retval; }; # # Add the nicks of users that you're chatting with to the msg history. # on #^msg 420 "*" { if (!CONFIG.TABKEY_MSGHIST_SENT_ONLY) { tabkey.msg_handler $servernum() $0; }; }; on #^send_msg 420 "*" { tabkey.msg_handler $servernum() $0; }; on #^dcc_chat 420 "*" { if (!CONFIG.TABKEY_MSGHIST_SENT_ONLY) { 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 (!CONFIG.TABKEY_MSGHIST_SENT_ONLY) { 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; }; };