Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

8667 lines (7603 sloc) 197.459 kb
" An IRC client plugin for Vim
" Maintainer: Madoka Machitani <madokam@zag.att.ne.jp>
" Created: Tue, 24 Feb 2004
" Last Change: Sat, 16 Apr 2005 09:57:57 +0900 (JST)
" License: Distributed under the same terms as Vim itself
"
" Credits:
" ircII the basics of IRC
" X-Chat ideas for DCC implementation specifically
" ERC ideas for netsplit handling, auto-away feature etc.
" KoRoN creator of a BBS viewer for Vim, Chalice (very popular
" among Japanese vim community)
" Ilya Sher an idea for mini-buffers for cmdline editing
" morbuz pointing out the error "E28: No such highlight group"
" with s:Hilite* functions
" alexander pointing out the excessive CPU-time consumption,
" feature of multiple server-connection on startup, etc
"
" Features:
" * real-time message receiving with user interaction (many of the normal
" mode commands available)
" * multiple server/channel connectivity
" * DCC SEND/CHAT functionalities
" * logging
" * command aliasing
" * auto-join channels on logon
" * auto-reconnect/rejoin after disconnect
" * auto-away
" * netsplit detection
"
" A Drawback:
" VimIRC achieves real-time message reception by implementing its own main
" loop. Therefore, while you are out of it, VimIRC has no way to get new
" messages, unless you move the cursor periodically.
"
" So my recommendation is, use a shortcut which creates a VimIRC-dedicated
" instance of Vim, where you do not initiate normal editing sessions (See
" Tip 1, located far below, for how to do this).
"
" Requirements:
" * Vim 6.2 or later with perl interface enabled
" * Perl 5.6 or later (5.8 or later if you want multibyte feature)
"
" Options:
"
" Basic settings:
" let g:vimirc_nick ="nickname"
" let g:vimirc_user ="username"
" let g:vimirc_realname ="full name"
" let g:vimirc_server ="irc.foobar.com:6667"
" (default: irc.freenode.net)
" Your favorite IRC server. Can be a
" comma-separated list.
" let g:vimirc_umode ="user modes" set upon logon (e.g.: "+i")
" let g:vimirc_pass ="password"
" ="password1@server1,password2@server2"
" Set this only if server requires
" authentication.
"
" Misc.:
" (Some of these options are configurable while running, with "/SET"
" command)
"
" let g:vimirc_partmsg ="message sent with QUIT/PART"
"
" let g:vimirc_autojoin ="#chan1,#chan2"
" ="#chan1|#chan2@irc.foo.com,#chan3@irc.bar.com"
"
" List of channels to join upon logon.
"
" You can specify a password by appending
" ":pass" to the channel, if it requires one.
"
" Special keyword "list" is also accepted, in
" which case the list of channels will be
" displayed upon logon.
"
" let g:vimirc_nickpass ="pass@irc.foo.com"
" ="nick:pass@irc.foo.com"
" ="nick1:pass1|nick2:pass2@irc.foo.com"
"
" List of passwords to identify yourself to
" NickServ.
"
" The first form can be used if you are using
" the same pass for different nicks, or you
" have only one nick registered.
"
" Use commas to separate settings for each
" server. (Only irc.freenode.net is
" supported currently)
"
" let g:vimirc_log =NUMBER
" Set this to non-zero to enable logging
" feature. (default: zero)
" Logs will be taken for each channel and
" server independently.
"
" Since server buffers are usually not
" interesting (is it?), they'll be logged only
" if NUMBER is greater than 1.
" let g:vimirc_logdir ="where log files are saved"
" (default: ~/.vimirc)
" The specified directory will be created
" automatically.
"
" let g:vimirc_listexpire =NUMBER
" (default: 604800 (a week))
" Threshold time in seconds after which VimIRC
" discards cached channels lists.
"
" Channels list is cached always (from
" 0.8.12), since obtaining one (command: /list)
" is rather a hard work for both server and
" client. Pressing "R" on the list buffer
" refreshes it anyway regardless of the value.
"
" let g:vimirc_browser ="web-browser"
" (default: see GetUserBrowser())
" The name of the web browser program which is
" to be invoked when hitting "<CR>" near a
" URL-like string.
"
" You can insert the special argument "%URL%",
" which will be replaced with the actual URL,
" so that you can add other arguments after it
" like this:
" "kterm -rv -e lynx %URL% &"
"
" let g:vimirc_winmode (abolished)
"
" let g:vimirc_infowidth =NUMBER
" (default: 20)
" Width of the info-bar (area to indicate
" which hidden buffers have new messages).
"
" Auto-away feature:
" let g:vimirc_autoaway =NUMBER
" Set to non-zero to enable the feature.
" (default: zero)
" let g:vimirc_autoawaytime =NUMBER
" Threshold time in seconds after which you
" will be marked as `away'
" (default: 1800 (30 minutes))
"
" DCC-related:
" let g:vimirc_dccdir ="where files are downloaded"
" (default: ~/.vimirc/dcc)
" let g:vimirc_dccport =NUMBER
" Port-number to watch at when you set up
" a dcc server. maybe necessary to set if you
" are behind firewall or something.
"
" Runtime Options:
" You can pass following options to the command :VimIRC, overriding the
" vim variables above
"
" -n nickname
" -u username
" -s server[:port]
" -p password
"
" Long option(s):
"
" --real[name]="full name"
"
" Startup:
" Type
" :VimIRC [runtime options]<CR>
"
" You will be prompted for several user information (nick etc.) if you have
" not set options listed above.
"
" Usage:
"
" Normal mode: This is a pseudo normal mode. Try your favorite Vim normal
" mode commands as usual.
"
" Hitting "i" or "I" will let you in the `command mode'
" described below, just as you do in Vim to go insert mode.
"
" ":", "/" and "?" keys will prompt you to enter ex-commands
" or search strings as usual.
"
" Hit <Ctrl-C> to get out of control and freely move around
" or do ex commands. Hit <Space> to re-enter the normal
" (online) mode again.
"
" Hitting "q" will quit the current channel, chat, server, or
" VimIRC itself, depending on the context ("Q" does the last
" forcibly). Contrarily, "r" and "R" keys would reconnect to
" servers, if they were disconnected.
"
" Special cases:
"
" * In a channels list window (which should open up with /list
" command), you can type "o" to sort the list, "O" to
" reverse it (I took these mappings from Mutt the e-mail
" client). "R" will refresh the list.
" Hitting "<CR>" will prompt you whether to join the channel
" where the cursor is.
"
" * In a nicks window, you can hit "<CR>" to choose an action
" to take against the one whom the cursor is on.
"
" * "<C-N>" / "<C-P>" keys are available in all buffers, for
" cycling forward/backward through channel/server buffers.
" "<C-W><C-N>" / "<C-W><C-P>" do the same, with windows
" split open.
"
" * Hit "<C-L>" if you find some windows have got corrupt
" window sizes. It will (hopefully) restore them.
"
" Command mode: This is just a normal buffer opened (at the bottom of the
" screen|below the current window). Enter IRC commands here.
" Hitting "<CR>", both in insert and normal mode, will send out
" the cursor line instantly either as a command or a message.
"
" Every IRC command starts with "/". E.g.: /join #vim,#c
"
" Line without a leading slash will be sent as a message,
" normaly to the current channel.
"
" Type /help<CR> to see the list of available commands. It's
" far from complete, though.
"
" Quit:
" Type
" /quit<CR>
" in the IRC command line to disconnect with the current server.
"
" To totally exit from VimIRC, press "Q" in normal mode, or type
" VimIRCQuit<CR>
" on the VIM command line.
"
" TODOs:
" * handling of control characters (bold, underline etc.)
" * multibyte support (done? I don't think so)
" * flood protection
" * IPv6
" * SSL
" * nicks auto-identification (done for freenode)
" * command-line completion (with tab, arrows etc.)
" * scripting (?)
" * help (both /help command and local help file)
" * menus (I personally never use menus)
" * etc. etc.
"
" Done:
" - command-line history (?)
" - handling of channel-mode changes
" - separate listing of channels with sorting facilities
" - auto reconnect/rejoin
" - ctcp, including dcc stuffs (well, mostly)
" - timer (it hasn't been in todo list though)
" - auto-away
" - logging
" - netsplit detection
" - command abbreviation (aliasing)
" - authentication (just add one line to send PASS)
"
" Tips:
" 1. If you see extreme slowness of vim's startup due to this plugin, put
" "let loaded_vimirc=1" in your .vimrc to avoid loading this in your
" everyday editing life. Create a VimIRC-dedicated rc file (.vimircrc
" or something) and put necessary settings into it. Then set up an
" alias (shortcut) which runs VimIRC, specifying the rc file you've just
" created. Like this:
" alias irc='gvim -i NONE -u ~/.vimircrc -c VimIRC'
"
" 2. In the `info-bar' on the left, you'll see buffer numbers next to the
" server/channel names. With them, you can easily navigate
" servers/channels like this:
" :sb1
"
" 3. You can send multiple messages/commands at a time. Prepare lines of
" messages elsewhere and copy/paste them into the command buffer. Then
" visually select the lines and press <CR>. (Do with caution so as not
" to be kicked off!)
"
" 4. How to send a message starting with a forward-slash? Just precede it
" with slash-space like this: "/ /message" (without quotes)
"
" 5. When writing in a channel, you can use '%' where you have to type the
" name of the current channel. Examples:
"
" /msg % You wouldn't normally write this way though.
" /msg %,nick This also works, at least theoretically.
" /notice % '%'s in the message won't be expanded.
" /topic % New Topic
" /invite nick %
"
" You can even omit '%' in the last two examples. Note also that '%'
" will be expanded to the nick, if you are on a chat window.
"
" 6. There is a special command prefix "SPLIT", which executes commands
" after it in new windows:
"
" /split /join #chan
" /sp freenode
"
" NOTE: The leading slash of the following command is optional. Note
" also that "split" can be abbreviated in the same manner as Vim's
" ":split".
"
" The second example shows that you can prepend it to aliases, too
" ("freenode" should expand into "/server irc.freenode.net" in this
" case).
"
" 7. Type shorter by utilizing aliases. Example:
"
" /alias sj split join
"
" registers new alias "sj", expanding to "split join". Now you can
" type:
"
" /sj #chan1,#chan2
"
" to open the channels in separate windows.
"
" 8. Many IRC commands are abbreviatable by default. E.g., "/join" can be
" typed in any way as "/joi", "jo", or "/j".
"
" Do "/h" to see how you can abbreviate commands.
"
" NOTE: ALIASes take precedence over ABBREVIATIONs. E.g., "/q" is an
" abbreviation of "/query", but you can (re)define "/q" as "/quit", if
" you prefer.
"
" 9. There are several ways to open (hidden) channel/chat/server buffers:
" (1) In normal mode (both online and offline), hit "<C-N>" and "<C-P>"
" to cycle through the buffers. It will open buffers in the same
" order shown in the info-bar on the left.
" You can prepend count: "2<C-N>" goes to the second next buffer
" counted from the current one.
" You can also prepend "<C-W>", to open the buffer in a new window
" (e.g., "<C-W><C-N>", "<C-W>2<C-N>", "2<C-W><C-N>").
" NOTE: Count will be multiplied if used both before and after
" "<C-W>".
" (2) Use ":buffer" command, as described in Tip 2.
" (3) Visit the info-bar (with "<C-W>t") and select the one you want to
" open, then hit "<CR>". "<C-W><CR>" will open it in a new window.
" (4) If you try to join already joined channels, either via "/join" or
" through channels-list, they'll just safely be opened.
if exists('g:loaded_vimirc') || &compatible
finish
endif
let s:version = '0.9.28'
let s:debug = (s:version =~# '-devel$')
if !s:debug
let g:loaded_vimirc = 1
endif
let s:save_cpoptions = &cpoptions
set cpoptions&
"
" Developing functions
"
if 0
" Truncate too long buffers (upon logging)
function! s:TruncateBuf()
" suggested option: vimirc_maxlines
endfunction
endif
"
" Start/Exit
"
function! s:GetUserInfo(args)
" NOTE: This may be called more than once
if !strlen(a:args)
\ && (exists('s:nick') && exists('s:user') && exists('s:realname'))
" Already set
return 1
endif
let s:nick = s:StrMatch(a:args, '-n\s*\(\S\+\)', '\1')
if !strlen(s:nick)
let s:nick = s:GetVimVar('g:vimirc_nick')
if !strlen(s:nick)
let s:nick = s:GetEnv('$IRCNICK')
if !strlen(s:nick)
let s:nick = s:Input('Enter your nickname')
endif
endif
endif
let s:user = s:StrMatch(a:args, '-u\s*\(\S\+\)', '\1')
if !strlen(s:user)
let s:user = s:GetVimVar('g:vimirc_user')
if !strlen(s:user)
let s:user = s:GetEnv('$USER')
if !strlen(s:user)
let s:user = s:Input('Enter your username')
endif
endif
endif
let s:pass = s:StrMatch(a:args, '-p\s*\(\S\+\)', '\1')
if !strlen(s:pass)
let s:pass = s:GetVimVar('g:vimirc_pass')
endif
let s:realname = s:StrMatch(a:args,
\"--real\\%(name\\)\\==\\(['\"]\\)\\(.\\{-\\}\\)\\1", '\2')
if !strlen(s:realname)
let s:realname = s:GetVimVar('g:vimirc_realname')
if !strlen(s:realname)
let s:realname = s:GetEnv('$NAME')
if !strlen(s:realname)
let s:realname = s:GetEnv('$IRCNAME')
if !strlen(s:realname)
let s:realname = s:Input('Enter your full name')
endif
endif
endif
endif
let s:umode = s:StrMatch(a:args, '-m\s*\(\S\+\)', '\1')
if !strlen(s:umode)
let s:umode = s:GetVimVar('g:vimirc_umode')
if !strlen(s:umode)
let s:umode = s:GetEnv('$IRCUMODE')
endif
endif
if !(strlen(s:nick) && strlen(s:user) && strlen(s:realname))
unlet s:nick s:user s:pass s:realname s:umode
return 0
endif
let s:server = s:StrMatch(a:args, '-s\s*\(\S\+\)', '\1')
if !strlen(s:server)
let s:server = s:GetVimVar('g:vimirc_server')
if !strlen(s:server)
let s:server = 'irc.freenode.net:6667'
endif
endif
return 1
endfunction
function! s:GetUserBrowser()
if !strlen(s:browser)
if s:IsWin3264()
let s:browser = 'start explorer'
elseif has('mac')
let s:browser = "osascript -e 'open location %URL%'"
elseif has('unix')
let s:browser = executable('mozilla')
\ ? 'mozilla'
\ : executable('netscape')
\ ? 'netscape'
\ : executable('lynx')
\ ? 'lynx'
\ : executable('w3m') ? 'w3m' : ''
endif
if !strlen(s:browser)
let s:browser = s:Input('Enter the name of your web browser')
call s:OptValidate('browser')
endif
endif
return s:browser
endfunction
function! s:GetServerOpt(option, server)
let option = a:option
" option1@server1,option2@server2
if a:option =~ '@'
let option = matchstr(a:option,
\ '\m[^,]\+\%(@\V'.a:server.'\m\%([,:]\|$\)\)\@=')
let option = substitute(option, '|', ',', 'g')
endif
return option
endfunction
function! s:GetServerUMODE(...)
let umode = s:GetServerOpt(s:umode, (a:0 ? a:1 : s:server))
if !strlen(umode)
" NOTE: This cannot be an empty string: required as a second parameter of
" USER command.
let umode = '0'
endif
return umode
endfunction
function! s:GetServerPASS(...)
return s:GetServerOpt(s:pass, (a:0 ? a:1 : s:server))
endfunction
function! s:InitVars()
if exists('s:sid') " already init'ed
return
endif
" Obtain the script ID
map <SID>xx <SID>xx
let s:sid = substitute(maparg('<SID>xx'), 'xx$', '', '')
unmap <SID>xx
let s:client = 'VimIRC '.s:version
" Set up the names of buffers we use
call s:InitBufNames()
" Init system variables
call s:ResetSysVars()
" User-defined options
" Favorite farewell message
call s:OptSet('vimirc_partmsg', (s:debug ? 'Testing ' : '').s:client.
\' (IRC client for Vim)')
" Preferred language. Encoding name which Perl's Encode module can accept
call s:OptSet('vimirc_preflang')
" On/off logging feature
call s:OptSet('vimirc_log', 0)
" Log directory
call s:OptSet('vimirc_logdir', expand('$HOME').'/.vimirc')
" Setings like aliases will go here
call s:OptSet('vimirc_rcfile', s:logdir.'/.vimircrc')
" Channels list will be refreshed after these amount of seconds have passed
call s:OptSet('vimirc_listexpire', (60 * 60 * 24 * 7))
" External Web browser
call s:OptSet('vimirc_browser')
" Directory where incoming dcc files should go
call s:OptSet('vimirc_dccdir', s:logdir.'/dcc')
" Port you want to listen to
call s:OptSet('vimirc_dccport')
" Window-related options
" Width of info-bar
call s:OptSet('vimirc_infowidth', 20)
" Width of nicks-window
call s:OptSet('vimirc_nickswidth', 12)
" Height of command-line buffer
call s:OptSet('vimirc_cmdheight', 1)
" Timer-related
" On/off auto-away feature
call s:OptSet('vimirc_autoaway', 0)
" Threshold time for you to be marked `away'. MUST be in seconds
call s:OptSet('vimirc_autoawaytime', (60 * 30))
endfunction
function! s:SetSysVars()
call s:ResetSysVars()
let s:opened = 1
" When the timer was last triggered
let s:lasttime = localtime()
" When user did some action most recently
let s:lastactive = s:lasttime
let s:lastbeep = s:lasttime
endfunction
function! s:ResetSysVars()
let s:opened = 0
let s:in_loop = 0
let s:autocmd_disabled = 0
let s:current_changed = 0
let s:split = 0
endfunction
function! s:SetGlobVars()
let s:eadirection = &eadirection
set eadirection=ver
let s:equalalways = &equalalways
let s:lazyredraw = &lazyredraw
set lazyredraw
let s:showbreak = &showbreak
let &showbreak = ' '
let s:statusline = &statusline
let &statusline = '%{'.s:sid.'GetBufTitle()}%=%l/%L'
let s:titlestring = &titlestring
let s:winminheight = &winminheight
set winminheight=1
let s:winwidth = &winwidth
set winwidth=12
endfunction
function! s:ResetGlobVars()
let &eadirection = s:eadirection
unlet s:eadirection
let &equalalways = s:equalalways
unlet s:equalalways
let &lazyredraw = s:lazyredraw
unlet s:lazyredraw
let &showbreak = s:showbreak
unlet s:showbreak
let &statusline = s:statusline
unlet s:statusline
let &titlestring = s:titlestring
unlet s:titlestring
let &winminheight = s:winminheight
unlet s:winminheight
let &winwidth = s:winwidth
unlet s:winwidth
endfunction
function! s:SetCmds()
if exists(':VimIRC')
delcommand VimIRC
endif
command! VimIRCQuit :call s:QuitVimIRC()
endfunction
function! s:ResetCmds()
if exists(':VimIRCQuit')
delcommand VimIRCQuit
endif
command! -nargs=* VimIRC :call s:StartVimIRC(<q-args>)
endfunction
if !exists(':VimIRC')
call s:ResetCmds()
endif
function! s:SetAutocmds()
augroup VimIRC
autocmd!
" NOTE: Cannot use CursorHold to auto re-enter the loop: getchar() won't
" get a char since key inputs will never be waited after that event.
execute 'autocmd CursorHold * call s:NotifyOffline()'
execute 'autocmd BufHidden' s:bufname_channel.'* call s:PreCloseBuf_Channel()'
execute 'autocmd BufHidden' s:bufname_chat.'* call s:PreCloseBuf_Chat()'
execute 'autocmd BufHidden' s:bufname_list.'* call s:PreCloseBuf_List()'
execute 'autocmd BufHidden' s:bufname_server.'* call s:PreCloseBuf_Server()'
execute 'autocmd BufUnload' s:bufname.'* call s:DelChanServ()'
execute 'autocmd BufWinEnter' s:bufname_channel.'* call s:PostOpenBuf_Channel()'
execute 'autocmd BufWinEnter' s:bufname_chat.'* call s:PostOpenBuf_Chat()'
execute 'autocmd BufWinEnter' s:bufname_list.'* call s:PostOpenBuf_List()'
execute 'autocmd BufWinEnter' s:bufname_server.'* call s:PostOpenBuf_Server()'
execute 'autocmd BufWinLeave' s:bufname.'* call s:ChangeChanServ(1)'
execute 'autocmd WinEnter' s:bufname.'* call s:ChangeChanServ(1)'
augroup END
endfunction
function! s:ResetAutocmds()
autocmd! VimIRC
endfunction
function! s:StartVimIRC(...)
if !has('perl')
echoerr 'To use this, you have to build vim with perl interface. Exiting.'
return
endif
if s:GetVimVar('s:opened')
return
endif
" Initialize most of the internal variables. User configurations will be
" dealt with here, too.
call s:InitVars()
" Parse command-line arguments
if !s:GetUserInfo(a:0 ? a:1 : '')
return
endif
" Load system .vimircrc
call s:RC_Load()
" Adjust some Vim's global variables to match VimIRC's need
call s:SetGlobVars()
" Set up system variables
call s:SetSysVars()
" Remove ":VimIRC" so this won't be called twice, etc.
call s:SetCmds()
call s:SetAutocmds()
" Set VimIRC's cursor color
call s:SetHlCursor()
call s:SetEncoding()
" Initialize perl codes
call s:PerlIRC()
" Now it is OK to commence connections
call s:StartServer()
call s:MainLoop()
endfunction
function! s:QuitVimIRC()
if !s:opened
return
endif
try
let s:autocmd_disabled = 1
call s:Send_GQUIT('QUIT', '')
call s:ResetCmds()
call s:ResetAutocmds()
call s:ResetGlobVars()
call s:ResetPerlVars()
call s:CloseWin_IRC()
if s:in_loop
throw 'IMGONNAQUIT'
endif
finally
call s:PromptKey(0, ' Thanks for flying VimIRC', 'Title')
call s:ResetSysVars()
endtry
endfunction
function! s:QuitWhat(severe)
if a:severe
" Don't confirm when quitting with "Q"
return s:QuitVimIRC()
endif
let canceld= 0
" NOTE: `server' could get invalid value
let server = s:GetVimVar('b:server')
let channel= s:GetVimVar('b:channel')
call s:SetCurServer(server)
if s:IsChannel(channel)
let canceld = !s:Confirm_YN('Really close channel '.channel)
if !canceld
return s:Send_PART('PART', channel)
endif
elseif s:IsNick(channel)
let canceld = !s:Confirm_YN('Really quit chat with '.channel)
if !canceld
return s:QuitChat(channel)
endif
endif
if s:IsConnected()
if s:Confirm_YN((canceld ? 'Then,'
\ : 'Really').' disconnect with server '.s:server)
call s:Send_QUIT('QUIT', '')
endif
else
if s:Confirm_YN((canceld ? 'Then,' : 'Really').' quit VimIRC')
call s:QuitVimIRC()
endif
endif
endfunction
function! s:MainLoop()
" NOTE: Be careful of the recursion, it may cause some obscure troubles
if !(!s:in_loop && s:opened && s:IsSockOpen())
return
endif
try
call s:PreMainLoop()
while 1
if getchar(1)
try
call s:HandleKey(getchar(0))
catch /^\S\+:E/
" Catch some familiar, or low severity errors
call s:IgnoreException(v:exception)
endtry
endif
if !s:DoTimer()
break
endif
endwhile
catch /^IMGONNA/
" Get out of the loop
" NOTE: You cannot see new messages posted while posting
catch /^Vim:Interrupt$/
if 1 && s:IsBufType_Command()
call s:DoInsert(0)
endif
catch
echoerr v:exception
finally
call s:PostMainLoop()
endtry
endfunction
function! s:IgnoreException(exception)
let errno = s:StrMatch(a:exception, ':E\(\d\+\):', '\1')
let ignore= (errno =~ '^\%(2[01]\|35\|78\|132\|4\%(43\|86\|92\)\)$')
if ignore
if errno != 132 " pressed the same key for a long time; ignore it
call s:EchoError('VimIRC: '.s:StrDivide(a:exception, 2))
endif
else
echoerr v:exception
endif
endfunction
function! s:PreMainLoop()
let s:in_loop = 1
let s:current_changed = 1
call s:ToggleCursor(1)
call s:ClearCommand(0)
" Show line-cursor, so the user can easily recognize that she is online
call s:HiliteLine('.')
endfunction
function! s:PostMainLoop()
call s:HiliteClear()
call s:ToggleCursor(0)
let s:in_loop = 0
endfunction
"
" .vimircrc manipulation
"
function! s:RC_Load()
if filereadable(s:rcfile)
execute 'source' s:rcfile
endif
endfunction
function! s:RC_Open(force)
return (a:force || filereadable(s:rcfile)) && s:OpenBuf('1split', s:rcfile)
endfunction
function! s:RC_Close()
if s:BufVisit(bufnr(s:rcfile))
if &modified
call s:ExecuteSilent('write!')
endif
call s:ExecuteSilent('bdelete!')
endif
endfunction
function! s:RC_Varname(type, name)
return 'vimirc_'.a:type.'_'.(a:name =~ '[^_[:alnum:]]'
\ ? '{"'.s:EscapeQuote(a:name).'"}' : a:name)
endfunction
function! s:RC_Section(section)
if !search('^" '.a:section, 'w')
call s:OpenNewLine()
call append('.', '" '.a:section)
$
endif
endfunction
function! s:RC_Set(type, name, value)
call s:RC_Unset(a:type, a:name)
let varname = s:RC_Varname(a:type, a:name)
call append('.', 'let '.varname.' = "'.s:EscapeQuote(a:value).'"')
let g:{varname} = a:value
endfunction
function! s:RC_Unset(type, name)
let varname = s:RC_Varname(a:type, a:name)
while search('\m'.s:EscapeMagic(varname).'\s*=', 'w')
call s:DelLine()
endwhile
unlet! g:{varname}
endfunction
"
" Buffer manipulation
"
function! s:InitBufNames()
let s:bufname = '_VimIRC_'
let list = 'info server list channel nicks command chat'
while strlen(list)
let type = s:StrDivide(list, 1)
let s:bufname_{type} = s:bufname.toupper(type).'_'
let list = s:StrDivide(list, 2)
endwhile
endfunction
" I'm using buffer numbers to access buffers: accessing by name will soon fail
" if user changes directory or something.
" NOTE: I removed the `server' argument from the functions below, just for
" ease of typing (esp. on the perl's side).
function! s:GetBufNum(bufname)
let bufnum = -1
let varname = 's:bufnum_'.a:bufname
if exists('{varname}')
if bufloaded({varname})
let bufnum = {varname}
else
unlet {varname}
endif
endif
return bufnum
endfunction
function! s:GetBufNum_Info()
return s:GetBufNum(s:GenBufName_Info())
endfunction
function! s:GetBufNum_Server(...)
return s:GetBufNum(s:GenBufName_Server(a:0 ? a:1 : s:server))
endfunction
function! s:GetBufNum_List(...)
return s:GetBufNum(s:GenBufName_List(a:0 ? a:1 : s:server))
endfunction
function! s:GetBufNum_Channel(channel, ...)
return s:GetBufNum(s:GenBufName_Channel(a:channel, (a:0 ? a:1 : s:server)))
endfunction
function! s:GetBufNum_Nicks(channel, ...)
return s:GetBufNum(s:GenBufName_Nicks(a:channel, (a:0 ? a:1 : s:server)))
endfunction
function! s:GetBufNum_Command(channel, ...)
return s:GetBufNum(s:GenBufName_Command(a:channel, (a:0 ? a:1 : s:server)))
endfunction
function! s:GetBufNum_Chat(nick, server)
return s:GetBufNum(s:GenBufName_Chat(a:nick, a:server))
endfunction
function! s:SetBufNum(bufname)
let s:bufnum_{a:bufname} = bufnr('%')
" Need to set this buffer as `current' if it is channel/server
call s:ChangeChanServ(0)
return s:bufnum_{a:bufname}
endfunction
function! s:DelBufNum(bufnum)
unlet! s:bufnum_{bufname(a:bufnum)}
endfunction
function! s:GenBufName_Info()
return s:bufname_info
endfunction
function! s:GenBufName_Server(...)
return s:bufname_server.(a:0 ? a:1 : s:server)
endfunction
function! s:GenBufName_List(...)
return s:bufname_list.(a:0 ? a:1 : s:server)
endfunction
function! s:GenBufName_Channel(channel, ...)
return s:bufname_channel.s:SecureChannel(a:channel).'@'.(a:0 ? a:1 : s:server)
endfunction
function! s:GenBufName_Nicks(channel, ...)
return s:bufname_nicks.s:SecureChannel(a:channel).'@'.(a:0 ? a:1 : s:server)
endfunction
function! s:GenBufName_Command(channel, ...)
return s:bufname_command.s:SecureChannel(a:channel).'@'.(a:0 ? a:1 : s:server)
endfunction
function! s:GenBufName_Chat(nick, server)
return s:bufname_chat.s:EscapeFName(a:nick).'@'.a:server
endfunction
function! s:SecureChannel(channel)
return (match(a:channel, '*') >= 0) ? '' : s:EscapeFName(tolower(a:channel))
endfunction
function! s:IsBufType_IRC(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && (!match(bufname(bufnum), s:bufname)
\ && s:ExistsBufVar(bufnum, 'server')))
endfunction
function! s:IsBufType_ChanChat(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && ( s:IsBufType_Channel(bufnum)
\ || s:IsBufType_Chat(bufnum)))
endfunction
" Whether this is an appropriate place over which to open another
" channel/server
function! s:IsBufType_ChanServ(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0
\ && ( s:IsBufType_Channel(bufnum) || s:IsBufType_Chat(bufnum)
\ || s:IsBufType_Server(bufnum) || s:IsBufType_List(bufnum)))
endfunction
function! s:IsBufType_Info(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && (s:IsBufType_IRC(bufnum)
\ && !match(bufname(bufnum), s:bufname_info)))
endfunction
function! s:IsBufType_Server(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && (s:IsBufType_IRC(bufnum)
\ && !match(bufname(bufnum), s:bufname_server)))
endfunction
function! s:IsBufType_List(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && (s:IsBufType_IRC(bufnum)
\ && !match(bufname(bufnum), s:bufname_list)))
endfunction
function! s:IsBufType_Channel(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && (s:IsBufType_IRC(bufnum)
\ && !match(bufname(bufnum), s:bufname_channel)))
endfunction
function! s:IsBufType_Nicks(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && (s:IsBufType_IRC(bufnum)
\ && !match(bufname(bufnum), s:bufname_nicks)))
endfunction
function! s:IsBufType_Command(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && (s:IsBufType_IRC(bufnum)
\ && !match(bufname(bufnum), s:bufname_command)))
endfunction
function! s:IsBufType_Chat(...)
let bufnum = a:0 ? a:1 : bufnr('%')
return (bufnum >= 0 && (s:IsBufType_IRC(bufnum)
\ && !match(bufname(bufnum), s:bufname_chat)))
endfunction
function! s:IsBufType_ServerDead()
return (s:IsBufType_IRC() && !(s:IsBufType_Chat() && b:channel[0] == '=')
\ && getbufvar(s:GetBufNum_Server(b:server), 'dead') + 0)
endfunction
function! s:IsServer(server)
" TODO: Be more precise (see RFC3986)
return (a:server =~ '[.:]')
endfunction
function! s:IsServerIRC(server)
return (a:server =~?
\'^\%(irc://\|\%(\w[-.[:alnum:]]\+[-.]\)\{-\}irc\)[^/]\+\%([/:]\S\+\)\=$')
endfunction
function! s:IsChannel(channel)
return !match(a:channel, '[&#+!]')
endfunction
function! s:IsNick(channel)
return (a:channel =~? '^=\=[-0-9\[-`a-z{-}]\+$')
endfunction
function! s:IsChanChat(channel)
return (s:IsChannel(a:channel) || s:IsNick(a:channel))
endfunction
function! s:IsList(channel)
return (a:channel ==# '*list')
endfunction
function! s:CanOpenChanServ()
let autocmd_disabled = s:autocmd_disabled
let s:autocmd_disabled = 1
if !(s:IsBufType_ChanServ() || s:VisitBuf_ChanServ(s:GetVimVar('b:channel'),
\ s:GetVimVar('b:server')))
let i = 1
while 1
let bufnum = winbufnr(i)
if (bufnum < 0) || (s:IsBufType_ChanServ(bufnum) && s:BufVisit(bufnum))
break
endif
let i = i + 1
endwhile
endif
let s:autocmd_disabled = autocmd_disabled
return s:IsBufType_ChanServ()
endfunction
" Change highlighting of the current buffer (later)
function! s:ChangeChanServ(auto)
if !((a:auto && s:autocmd_disabled) || s:current_changed)
let s:current_changed = s:IsBufType_ChanServ(a:auto
\ ? expand('<abuf>') + 0
\ : bufnr('%'))
endif
endfunction
function! s:EnterChanServ(split)
let line = getline('.')
let rx = '^\s*[-+*]\=\[-\=\d\+\]\(\S\+\)\%(\s\+\(\S\+\)\)\=$'
if line =~ rx
let channel= s:StrMatch(line, rx, '\1')
let server = s:StrMatch(line, rx, '\2')
call s:OpenChanServ(channel, (!strlen(server) ? channel : server), a:split)
endif
endfunction
function! s:OpenChanServ(channel, server, split)
let s:server = a:server
let s:split = a:split
if s:IsChannel(a:channel)
call s:OpenBuf_Channel(a:channel)
elseif s:IsNick(a:channel)
call s:OpenBuf_Chat(a:channel, a:server)
elseif s:IsList(a:channel)
call s:OpenBuf_List()
else
call s:OpenBuf_Server()
endif
let s:split = 0
endfunction
function! s:OpenNextChanServ(forward, split, ...)
if s:CanOpenChanServ()
let bufnum = s:GetBufNum_Next(a:forward, (a:0 ? a:1 : v:count1))
if bufnum
call s:OpenChanServ(getbufvar(bufnum, 'channel'),
\ getbufvar(bufnum, 'server'),
\ (a:split * (a:forward ? 1 : -1)))
endif
endif
endfunction
function! s:VisitBuf_ChanChat(channel, ...)
return (strlen(a:channel)
\ && s:VisitBuf_Cha{s:IsNick(a:channel)
\ ? 't' : 'nnel'}(a:channel, (a:0 ? a:1 : s:server)))
endfunction
function! s:VisitBuf_ChanServ(channel, ...)
let server = a:0 ? a:1 : s:server
return s:IsChanChat(a:channel)
\ ? s:VisitBuf_ChanChat(a:channel, server)
\ : s:VisitBuf_ServList(server)
endfunction
function! s:VisitBuf_ServList(...)
let server = a:0 ? a:1 : s:server
return (s:VisitBuf_Server(server) || s:VisitBuf_List(server))
endfunction
function! s:VisitBuf_Info()
return s:BufVisit(s:GetBufNum_Info())
endfunction
function! s:VisitBuf_Server(...)
return s:BufVisit(s:GetBufNum_Server(a:0 ? a:1 : s:server))
endfunction
function! s:VisitBuf_List(...)
return s:BufVisit(s:GetBufNum_List(a:0 ? a:1 : s:server))
endfunction
function! s:VisitBuf_Channel(channel, ...)
return s:BufVisit(s:GetBufNum_Channel(a:channel, (a:0 ? a:1 : s:server)))
endfunction
function! s:VisitBuf_Chat(nick, ...)
return s:BufVisit(s:GetBufNum_Chat(a:nick, (a:0 ? a:1 : s:server)))
endfunction
function! s:VisitBuf_Nicks(channel, ...)
return s:BufVisit(s:GetBufNum_Nicks(a:channel, (a:0 ? a:1 : s:server)))
endfunction
"
" Opening buffers
"
function! s:OpenBuf(comd, ...)
try
let equalalways = &equalalways
let winminheight= &winminheight
let winminwidth = &winminwidth
let curbuf = bufnr('%')
let winline = winline()
let &equalalways = 0
" Avoid "not enough room" error
set winminheight=0 winminwidth=0
call s:ExecuteSilent(a:comd.(a:0 && strlen(a:1) ? ' '.a:1 : ''))
setlocal noswapfile modifiable
if !s:autocmd_disabled
call s:RestoreWinLine(curbuf, winline)
endif
catch
if s:debug
echoerr v:exception
endif
finally
let &equalalways = equalalways
let &winminheight = winminheight
let &winminwidth = winminwidth
return !strlen(v:exception)
endtry
endfunction
function! s:OpenBufNum(comd, bufnum)
return (a:bufnum > 0 && s:OpenBuf(a:comd, '+'.a:bufnum.'buffer'))
endfunction
function! s:OpenBuf_Info()
let bufnum = s:GetBufNum_Info()
let loaded = (bufnum >= 0)
let retval = (loaded && s:BufVisit(bufnum))
if !retval
let comd = 'vertical topleft '.s:infowidth.'split'
if loaded
call s:OpenBufNum(comd, bufnum)
else
let bufname = s:GenBufName_Info()
call s:OpenBuf(comd, bufname)
call s:InitBuf_Info(bufname)
endif
let retval = -1
endif
return retval
endfunction
function! s:OpenBuf_Server(...)
let bufnum = s:GetBufNum_Server()
let loaded = (bufnum >= 0)
" No need to search the buffer when peeking: it's already done
if s:autocmd_disabled || !(loaded && s:BufVisit(bufnum))
" Just stay here if it is peek mode
if !s:autocmd_disabled
" Reuse channels list window, if found
if !s:BufVisit(s:GetBufNum_List())
call s:CanOpenChanServ()
endif
if s:split
call s:SplitBuf_Channel()
endif
endif
if loaded
call s:OpenBuf('buffer', bufnum)
else
let bufname = s:GenBufName_Server()
call s:OpenBuf('edit', bufname)
call s:InitBuf_Server(bufname, (a:0 ? a:1 : 0))
endif
endif
if loaded && !s:autocmd_disabled
if a:0
" a:1 is given only when user executed /server command, in which case we
" must clear the dead state of this buffer.
call setbufvar(bufnum, 'dead', 0)
endif
call s:WinBottom('$')
endif
endfunction
function! s:OpenBuf_List()
let bufnum = s:GetBufNum_List()
let loaded = (bufnum >= 0)
if s:autocmd_disabled || !(loaded && s:BufVisit(bufnum))
" Open it next to, or on the server window
if !s:autocmd_disabled
if !s:BufVisit(s:GetBufNum_Server())
call s:CanOpenChanServ()
endif
if s:split
" I don't like vertical split, which was the original behaviour
call s:SplitBuf_Channel()
endif
endif
if loaded
call s:OpenBuf('buffer', bufnum)
else
let bufname = s:GenBufName_List()
call s:OpenBuf('edit', bufname)
call s:InitBuf_List(bufname)
endif
endif
if loaded && !s:autocmd_disabled
call s:DoNormal('^')
endif
endfunction
function! s:OpenBuf_Channel(channel)
let bufnum = s:GetBufNum_Channel(a:channel)
let loaded = (bufnum >= 0)
if s:autocmd_disabled || !(loaded && s:BufVisit(bufnum))
if !s:autocmd_disabled
call s:CanOpenChanServ()
if s:split
call s:SplitBuf_Channel()
endif
endif
if loaded
call s:OpenBuf('buffer', bufnum)
call s:RestorePrevLine()
else
let bufname = s:GenBufName_Channel(a:channel)
call s:OpenBuf('edit', bufname)
call s:InitBuf_Channel(bufname, a:channel)
endif
" XXX: Is this necessary? Or necessary to all other OpenBuf_ funcs?
if &l:winfixheight
let &l:winfixheight = 0
endif
endif
endfunction
function! s:OpenBuf_Chat(nick, server)
let bufnum = s:GetBufNum_Chat(a:nick, a:server)
let loaded = (bufnum >= 0)
if s:autocmd_disabled || !(loaded && s:BufVisit(bufnum))
if !s:autocmd_disabled
call s:CanOpenChanServ()
if s:split
call s:SplitBuf_Channel()
endif
endif
if loaded
call s:OpenBuf('buffer', bufnum)
call s:RestorePrevLine()
else
let bufname = s:GenBufName_Chat(a:nick, a:server)
call s:OpenBuf('edit', bufname)
call s:InitBuf_Chat(bufname, a:nick, a:server)
endif
endif
endfunction
function! s:OpenBuf_Nicks(channel, ...)
let server = a:0 ? a:1 : s:server
let retval = s:VisitBuf_ChanChat(a:channel, server)
if retval
let bufnum = s:GetBufNum_Nicks(a:channel, server)
let loaded = (bufnum >= 0)
if !(loaded && s:BufVisit(bufnum))
let comd = 'vertical belowright '.s:nickswidth.'split'
if loaded
call s:OpenBufNum(comd, bufnum)
else
let bufname = s:GenBufName_Nicks(a:channel, server)
call s:OpenBuf(comd, bufname)
call s:InitBuf_Nicks(bufname, a:channel, server)
endif
endif
endif
return retval
endfunction
function! s:OpenBuf_Command(posting)
if !(s:opened && s:IsBufType_IRC())
return
endif
" Set the current server name here, so the buffer number/name will be
" generated properly
if a:posting
call s:SetCurServer(b:server)
endif
let channel = s:GetVimVar('b:channel')
let bufnum = s:GetBufNum_Command(channel)
let loaded = (bufnum >= 0)
if !s:IsBufCurrent(bufnum)
call s:VisitBuf_ChanServ(channel)
endif
" This is necessary for opening it in a desired height
let &equalalways = 0
if !(loaded && s:BufVisit(bufnum))
let comd = 'belowright '.s:cmdheight.'split'
if loaded
call s:OpenBufNum(comd, bufnum)
else
let bufname = s:GenBufName_Command(channel)
call s:OpenBuf(comd, bufname)
call s:InitBuf_Command(bufname, channel)
endif
endif
if !&l:winfixheight
setlocal winfixheight
endif
if a:posting
call s:OpenNewLine()
call s:DoInsert(0)
endif
endfunction
function! s:PostOpenBuf_Server()
if s:autocmd_disabled
return
endif
let abuf = expand('<abuf>') + 0
let server = getbufvar(abuf, 'server')
if strlen(server)
call s:ResetCurChanServ('', server)
endif
endfunction
function! s:PostOpenBuf_List()
if s:autocmd_disabled
return
endif
let abuf = expand('<abuf>') + 0
let channel = getbufvar(abuf, 'channel')
let server = getbufvar(abuf, 'server')
if strlen(server)
call s:ResetCurChanServ(channel, server)
endif
endfunction
function! s:PostOpenBuf_Channel()
if s:autocmd_disabled
return
endif
let abuf = expand('<abuf>') + 0
let channel = getbufvar(abuf, 'channel')
let server = getbufvar(abuf, 'server')
if strlen(channel) && strlen(server)
let donick = (s:OpenBuf_Nicks(channel, server) && s:IsBufEmpty())
call s:BufVisit(abuf)
call s:ResetCurChanServ(channel, server, donick)
endif
endfunction
function! s:PostOpenBuf_Chat()
if s:autocmd_disabled
return
endif
let abuf = expand('<abuf>') + 0
let nick = getbufvar(abuf, 'channel')
let server= getbufvar(abuf, 'server')
if strlen(nick) && strlen(server)
call s:OpenBuf_Nicks(nick, server)
call s:OpenBuf_Command(0)
call s:BufVisit(abuf)
call s:ResetCurChanServ(nick, server)
endif
endfunction
function! s:SplitBuf_Channel()
let autocmd_disabled = s:autocmd_disabled
let s:autocmd_disabled = 1
let below = (s:split > 0)
let hasnick = s:IsBufType_ChanChat()
if hasnick
call s:CloseBuf_Nicks(b:channel, b:server)
call s:CloseBuf_Command(b:channel, b:server)
endif
if !&equalalways
let &equalalways = 1
endif
if s:OpenBuf((below ? 'belowright' : 'aboveleft').' split')
call s:DoWincmd(below ? 'k' : 'j')
if !s:IsBufType_List()
call s:WinScroll2('.')
endif
if hasnick
call s:OpenBuf_Nicks(b:channel, b:server)
endif
call s:DoWincmd(below ? 'j' : 'k')
" New channel will be opened here
endif
let s:autocmd_disabled = autocmd_disabled
endfunction
function! s:ModifyBuf(modify, ...)
" XXX: This should NOT be called recursively
let bufnum = a:0 ? a:1 : bufnr('%')
if a:modify
if !exists('s:save_undolevels')
let s:save_undolevels = &undolevels
set undolevels=-1
endif
call setbufvar(bufnum, '&modifiable', 1)
else
if exists('s:save_undolevels')
let &undolevels = s:save_undolevels
unlet s:save_undolevels
endif
if 0 && !s:IsBufType_Command(bufnum)
call setbufvar(bufnum, '&modifiable', 0)
endif
endif
endfunction
function! s:PeekBuf(orgwin)
if !a:orgwin
call s:PrePeekBuf()
else
call s:PostPeekBuf(a:orgwin)
endif
endfunction
function! s:PrePeekBuf()
let s:autocmd_disabled = 1
let &equalalways = 0
call s:CanOpenChanServ() " XXX: is this necessary?
call s:OpenBuf('vertical 1split')
endfunction
function! s:PostPeekBuf(orgwin)
call s:ExecuteSilent('close!')
call s:WinVisit(a:orgwin)
let s:autocmd_disabled = 0
endfunction
"
" Buffer initilization
"
function! s:RegistMap(key, func, ...)
let keylist = a:key
while strlen(keylist)
let key = s:StrDivide(keylist, 1)
let modelist = a:0 ? a:1 : 'n'
while strlen(modelist)
let mode = s:StrDivide(modelist, 1)
execute mode.'noremap <buffer> <silent>' (key)
\ (mode == 'i' ? '<Esc>' : '').':'.
\ (mode != 'v' ? '<C-U>' : '').'call <SID>'.a:func.'<CR>'
let modelist = s:StrDivide(modelist, 2)
endwhile
let keylist = s:StrDivide(keylist, 2)
endwhile
endfunction
function! s:UnregistMap(key)
let keylist = a:key
while strlen(keylist)
call s:ExecuteSilent('unmap <buffer> '.s:StrDivide(keylist, 1))
let keylist = s:StrDivide(keylist, 2)
endwhile
endfunction
function! s:DoSettings()
setlocal bufhidden=hide
setlocal buftype=nofile
setlocal nolist
setlocal nonumber
setlocal noswapfile
setlocal wrap
call s:RegistMap('<Space>', 'MainLoop()')
call s:RegistMap('<CR>', 'HandleEnter(0)')
call s:RegistMap('<C-CR>,<S-CR>,<C-W><CR>,<C-W><C-CR>', 'HandleEnter(1)')
call s:RegistMap('a,A,i,I,o,O', 'OpenBuf_Command(1)')
call s:RegistMap('p,P', 'Beep(1)', 'n,v')
call s:RegistMap('q,ZZ', 'QuitWhat(0)')
call s:RegistMap('Q,ZQ', 'QuitWhat(1)')
call s:RegistMap('r', 'ReconnServer(0)')
call s:RegistMap('R', 'ReconnServer(1)')
call s:RegistMap('<C-L>', 'RestoreWinSize(1)')
call s:RegistMap('<C-N>', 'OpenNextChanServ(1, 0)')
call s:RegistMap('<C-P>', 'OpenNextChanServ(0, 0)')
call s:RegistMap('<C-W><C-N>', 'OpenNextChanServ(1, 1)')
call s:RegistMap('<C-W><C-P>', 'OpenNextChanServ(0, 1)')
call s:RegistMap('<F1>', 'Cmd_HELP()')
call s:HiliteClear()
endfunction
function! s:DoSyntax()
" NOTE: I'm really bad at syntax highlighting. It's horrible
" NOTE: Do not overdo. It'll slow things down
syntax match VimIRCUserHead display "^\S\+\%( \S\+:\)\=" contains=@VimIRCUserName
syntax match VimIRCTime display "^\d\d:\d\d" containedin=VimIRCUserHead contained
syntax match VimIRCBullet display "[*!]" containedin=VimIRCUserHead contained
" User names
syntax cluster VimIRCUserName contains=VimIRCUserPrivMSG,VimIRCUserNotice,VimIRCUserAction,VimIRCUserQuery
syntax match VimIRCUserPrivMSG display "<\S\+>" contained
syntax match VimIRCUserNotice display "\[\S\+\]" contained
syntax match VimIRCUserAction display "\*\S\+\*" contained
syntax match VimIRCUserQuery display "?\S\+?" containedin=VimIRCUserHead contained
syntax match VimIRCUnderline display ".\{-\}" contains=VimIRCIgnore
syntax match VimIRCBold display ".\{-\}" contains=VimIRCIgnore
syntax match VimIRCIgnore display "[]" contained
highlight link VimIRCTime String
highlight link VimIRCUserHead PreProc
highlight link VimIRCBullet WarningMsg
highlight link VimIRCUserPrivMSG Identifier
highlight link VimIRCUserNotice Statement
highlight link VimIRCUserAction WarningMsg
highlight link VimIRCUserQuery Question
highlight link VimIRCUnderline Underlined
" FIXME: This doesn't work
highlight link VimIRCIgnore Ignore
highlight VimIRCBold gui=bold cterm=bold term=bold
endfunction
function! s:DoSyntax_Info()
syntax match VimIRCInfoHasNew "^\s*+.*$" contains=VimIRCInfoIndic,VimIRCInfoBufNum
syntax match VimIRCInfoActive "^\s*\*.*$" contains=VimIRCInfoIndic,VimIRCInfoBufNum
syntax match VimIRCInfoDead "^\s*-.*$" contains=VimIRCInfoIndic,VimIRCInfoBufNum
syntax match VimIRCInfoIndic "^\s*[*+-]" contained
syntax match VimIRCInfoBufNum "\[-\=\d\+\]"
highlight link VimIRCInfoHasNew DiffChange
highlight link VimIRCInfoDead Comment
highlight link VimIRCInfoIndic Ignore
highlight link VimIRCInfoBufNum Comment
execute 'highlight link VimIRCInfoActive '.s:hl_cursor
endfunction
function! s:DoSyntax_Server()
call s:DoSyntax_Channel()
syntax match VimIRCWallop display "!\S\+!" contained containedin=VimIRCUserHead
highlight link VimIRCWallop WarningMsg
endfunction
function! s:DoSyntax_List()
syntax match VimIRCListChan "^[&#+!]\S\+\s\+\d\+" contains=VimIRCListMember
syntax match VimIRCListMember "\<\d\+\>" contained
highlight link VimIRCListChan Identifier
highlight link VimIRCListMember Number
endfunction
function! s:DoSyntax_Channel()
call s:DoSyntax()
" FIXME: Highlight for chanops disappears sometimes after buffer reopen, why?
syntax match VimIRCChanEnter display "->" contained containedin=VimIRCUserHead
syntax match VimIRCChanExit display "<[-=]" contained containedin=VimIRCUserHead
syntax match VimIRCChanPriv display "[@+]" contained containedin=@VimIRCUserName
highlight link VimIRCChanEnter DiffChange
highlight link VimIRCChanExit DiffDelete
highlight link VimIRCChanPriv Statement
endfunction
function! s:DoSyntax_Nicks()
syntax match VimIRCNicksChop "^@"
syntax match VimIRCNicksVoice "^+"
highlight link VimIRCNicksChop Identifier
highlight link VimIRCNicksVoice Statement
endfunction
function! s:DoSyntax_Chat()
call s:DoSyntax()
syntax match VimIRCUserChat display "=\S\+=" containedin=VimIRCUserHead contained
highlight link VimIRCUserChat Special
endfunction
function! s:InitBuf_Info(bufname)
let b:server = 'Connections'
let b:title = ' '.b:server
call s:DoSettings()
call s:DoSyntax_Info()
setlocal nowrap
call s:SetBufNum(a:bufname)
endfunction
function! s:InitBuf_Server(bufname, port)
let b:server = s:server
let b:port = a:port
call setline(1, s:GetTime(1).' *: Connecting with '.s:server.'...')
call s:DoSettings()
call s:DoSyntax_Server()
call s:SetBufNum(a:bufname)
call s:SetUserMode('')
endfunction
function! s:InitBuf_List(bufname)
let b:server = s:server
let b:channel = '*list'
let b:title = ' List of channels @ '.s:server
let b:updated = 0
let b:updating= 1
call s:DoSettings()
call s:DoSyntax_List()
setlocal nowrap
" I take Mutt's key-bindings for sorting
call s:RegistMap('o', 'SortSelect()')
call s:RegistMap('O', 'SortReverse()')
call s:RegistMap('R', 'UpdateList()')
call s:SetBufNum(a:bufname)
endfunction
function! s:InitBuf_Channel(bufname, channel)
let b:server = s:server
let b:channel = a:channel
let b:cmode = ''
let b:topic = ''
let b:title = ' '.a:channel.' @ '.s:server
call setline(1, s:GetTime(1).' *: Now talking in '.a:channel)
call s:DoSettings()
call s:DoSyntax_Channel()
call s:SetBufNum(a:bufname)
endfunction
function! s:InitBuf_Chat(bufname, nick, server)
" NOTE: a:server is the name of the IRC server, not the dcc peer's
let b:server = a:server
let b:channel = a:nick
let b:title = ' Chatting with '.a:nick
call setline(1, s:GetTime(1).' *: Now chatting with '.a:nick)
call s:DoSettings()
call s:DoSyntax_Chat()
let bufnum = s:SetBufNum(a:bufname)
if !s:autocmd_disabled && s:OpenBuf_Nicks(a:nick, a:server)
call s:BufVisit(bufnum)
endif
endfunction
function! s:InitBuf_Nicks(bufname, channel, server)
let b:server = a:server
let b:channel = a:channel
let b:title = a:channel
call s:DoSettings()
call s:DoSyntax_Nicks()
setlocal nowrap
call s:RegistMap('<CR>', 'SelectNickAction()', 'n,v')
call s:SetBufNum(a:bufname)
if s:IsNick(a:channel)
call s:FillBuf_Nicks(a:channel, a:server)
endif
endfunction
function! s:InitBuf_Command(bufname, channel)
let b:server = s:server
let b:channel = a:channel
let b:title = ' '.(s:IsNick(a:channel)
\ ? 'Chatting with'
\ : 'Posting to').' '.(s:IsChanChat(a:channel)
\ ? a:channel.' @ ' : '').s:server
call s:DoSettings()
setlocal expandtab
call s:RegistMap('<CR>', 'SendLines()', 'n,i,v')
call s:UnregistMap('a,A,i,I,o,O,p,P')
call s:SetBufNum(a:bufname)
endfunction
function! s:FillBuf_Nicks(nick, server)
call s:ModifyBuf(1)
call s:BufClear()
call setline(1, a:nick)
call append(1, s:GetCurNick(a:server))
call s:ModifyBuf(0)
endfunction
"
" Closing buffers
"
function! s:PreCloseBuf_Server()
if s:autocmd_disabled
return
endif
let abuf = expand('<abuf>') + 0
let server = getbufvar(abuf, 'server')
if strlen(server) " validity check
call s:CloseBuf_Command('', server)
endif
endfunction
function! s:PreCloseBuf_List()
if s:autocmd_disabled
return
endif
let abuf = expand('<abuf>') + 0
let server = getbufvar(abuf, 'server')
if strlen(server) && !s:VisitBuf_Server(server)
call s:CloseBuf_Command('', server)
endif
endfunction
function! s:PreCloseBuf_Channel()
if s:autocmd_disabled
return
endif
let abuf = expand('<abuf>') + 0
let channel = getbufvar(abuf, 'channel')
let server = getbufvar(abuf, 'server')
if strlen(channel) && strlen(server)
call s:CloseBuf_Nicks(channel, server)
call s:CloseBuf_Command(channel, server)
endif
endfunction
function! s:PreCloseBuf_Chat()
if s:autocmd_disabled
return
endif
let abuf = expand('<abuf>') + 0
let nick = getbufvar(abuf, 'channel')
let server= getbufvar(abuf, 'server')
if strlen(nick) && strlen(server)
call s:CloseBuf_Nicks(nick, server)
call s:CloseBuf_Command(nick, server)
endif
endfunction
function! s:PreCloseBuf_Command(destbuf, purge)
" Do NOT define any autocmd events to trigger this
if s:autocmd_disabled
return
endif
let cmdbuf = a:destbuf ? s:GetBufNum_Command(s:channel)
\ : expand('<abuf>') + 0
let opened = s:BufVisit(cmdbuf)
if opened || s:OpenBufNum('belowright 1split', cmdbuf)
call s:NeatenBuf_Command(a:purge)
if a:destbuf
if opened && s:IsNick(s:channel) " keep it open when chatting
call s:RedrawScreen(0)
else
let s:autocmd_disabled = 1
call s:BufClose(cmdbuf)
let s:autocmd_disabled = 0
endif
call s:PostCloseBuf_Command(cmdbuf, a:destbuf)
endif
endif
endfunction
function! s:CloseBuf_Server()
" Close associated windows before closing the server window
call s:CloseBuf_List()
let bufnum = s:GetBufNum_Server()
if bufnum >= 0
if s:log >= 2
call s:LogBuffer(bufnum)
endif
" Mark this buffer as dead so that the next /SERVER invocation can close
" this
call setbufvar(bufnum, 'dead', 1)
endif
endfunction
function! s:CloseBuf_List()
let bufnum = s:GetBufNum_List()
if bufnum >= 0
call s:CacheList(bufnum)
call s:OpenBuf_Server()
call s:BufClose(bufnum)
endif
endfunction
function! s:CloseBuf_Channel(channel)
let bufnum = s:GetBufNum_Channel(a:channel)
if bufnum >= 0
call s:LogBuffer(bufnum)
if !s:VisitBuf_ServList()
call s:OpenBuf_Server()
endif
call s:BufClose(bufnum)
if !s:IsBufType_List()
call s:WinScroll2('.')
endif
endif
endfunction
function! s:CloseBuf_Chat(nick, server)
let save_server = s:server
let s:server = a:server
let bufnum = s:GetBufNum_Chat(a:nick, a:server)
if bufnum >= 0
call s:LogBuffer(bufnum)
if !s:VisitBuf_ServList()
call s:OpenBuf_Server()
endif
call s:BufClose(bufnum)
if !s:IsBufType_List()
call s:WinScroll2('.')
endif
endif
let s:server = save_server
endfunction
function! s:CloseBuf_Nicks(channel, server)
call s:BufClose(s:GetBufNum_Nicks(a:channel, a:server))
endfunction
function! s:CloseBuf_Command(channel, server)
call s:BufClose(s:GetBufNum_Command(a:channel, a:server))
endfunction
function! s:PostCloseBuf_Command(cmdbuf, destbuf)
if a:destbuf != a:cmdbuf
" Move the cursor to the destined place
call s:BufVisit(a:destbuf)
else
" Move the cursor back onto the channel/server where command mode was
" (supposedly) triggered.
call s:VisitBuf_ChanServ(s:channel)
endif
if !s:IsBufType_List()
" XXX: I do this to prevent channels from unexpectedly scrolling UP due
" to BufClose above.
call s:WinScroll2('.')
endif
endfunction
" Make the command buffer look like a command-line history
function! s:NeatenBuf_Command(purge)
call s:ModifyBuf(1)
call s:HiliteClear()
if a:purge
" We move the input line to the bottom
let line = getline('.')
if strlen(line)
call s:DelLine()
" Remove duplicates, if any
while s:SearchLine(line)
call s:DelLine()
endwhile
call {strlen(getline('$')) ? 'append' : 'setline'}('$', line)
endif
endif
call s:BufTrim()
call s:OpenNewLine()
call s:ModifyBuf(0)
endfunction
"
" Closing windows
"
function! s:CloseWin_What(cond)
let retval = 1
let curbuf = bufnr('%')
let s:autocmd_disabled = 1
call s:DoWincmd('b')
while 1
" TODO: Accept arguments and/or multiple conditions?
if s:{a:cond}()
" XXX: Vim crashes here sometimes
if !s:WinClose()
let retval = 0
break
endif
elseif winnr() == 1
break
else
call s:DoWincmd('W')
endif
endwhile
call s:BufVisit(curbuf)
let s:autocmd_disabled = 0
return retval
endfunction
function! s:CloseWin_IRC()
if !s:CloseWin_What('IsBufType_IRC')
enew!
endif
call s:RedrawScreen(0)
endfunction
function! s:CloseWin_DeadServer()
call s:CloseWin_What('IsBufType_ServerDead')
call s:ClearCommand(0)
call s:RedrawScreen(0)
endfunction
"
" Restoring windows
"
function! s:GetPrevLine()
return line("'\"")
endfunction
" Restore the previous cursor position after re-opening a buffer
function! s:RestorePrevLine()
let lnum = s:GetPrevLine()
if s:autocmd_disabled
if lnum > 0 && line('.') != lnum
execute lnum
endif
else
call s:WinScroll2(lnum)
endif
endfunction
" Tackle the annoyance of unexpected window scrolling when opening another
" buffer
function! s:RestoreWinLine(bufnum, winline)
let curwin = winnr()
if s:BufVisit(a:bufnum)
call s:WinScroll(winline() - a:winline)
call s:WinVisit(curwin)
endif
endfunction
function! s:RestoreWinSize(restore)
if !s:opened
return
endif
let s:autocmd_disabled = 1
if a:restore
let orgwin = winnr()
let prevwin = s:GetWinNum_Prev(0)
let equalalways = s:ToggleEqualWin(1)
endif
let botwin = s:DoWincmd('b')
call s:DoWincmd('t')
while 1
if s:IsBufType_Nicks()
call s:WinResize(s:nickswidth, 1)
elseif s:IsBufType_Command()
call s:WinResize(s:cmdheight, 0)
elseif s:IsBufType_Info()
call s:WinResize(s:infowidth, 1)
endif
if s:IsBufType_ChanServ() && !s:IsBufType_List()
" XXX: Window could unexpectedly scroll up due to resizing
call s:WinScroll2('.')
else
call s:DoNormal('^')
endif
if winnr() == botwin
break
endif
call s:DoWincmd('w')
endwhile
if a:restore
call s:WinVisit(prevwin)
call s:WinVisit(orgwin)
call s:RedrawScreen(1)
let &equalalways = equalalways
endif
let s:autocmd_disabled = 0
endfunction
function! s:GetWinNum_Prev(restore)
let curwin = winnr()
call s:DoWincmd('p')
let prevwin = winnr()
if a:restore
call s:DoWincmd('p')
endif
return prevwin - ((curwin > 1 && curwin == prevwin) ? 1 : 0)
endfunction
function! s:ToggleEqualWin(on)
let equalalways = &equalalways
if a:on
let &equalalways = !&equalalways
endif
let &equalalways = a:on
return equalalways
endfunction
"
" Providing some user interaction
"
function! s:HandleAt(comd)
if a:comd =~ '[:=]$'
let ex = (a:comd =~ ':$')
if ex
let comd = histget('@', -1)
else
call s:RedrawScreen(0)
let comd = substitute(s:DoInput(0, '=', ''), "\\%(^[\"']\\|[\"']$\\)",
\ '', 'g')
endif
if s:DoIterate(ex, comd, s:ComputeCount(a:comd))
call s:PromptEnter(2)
endif
else
call s:DoNormal(a:comd, 1)
endif
endfunction
function! s:HandleEnter(shifted, ...)
if s:IsBufType_Info()
return s:EnterChanServ(a:shifted)
elseif !a:shifted
if s:IsBufType_Command()
return s:SendLines()
elseif s:IsBufType_Nicks()
return s:SelectNickAction()
endif
endif
return s:OpenLink(a:shifted, (a:0 ? a:1 : 1))
endfunction
function! s:HandleKey(key)
let char = s:Key2Char(a:key)
if !a:key
call s:UnstickKey(0)
endif
call s:HiliteClear()
" TODO: User-configurable maps
" Place movement commands earlier, to get quick response
if (a:key >= 2 && a:key <= 10)
\ || char =~# '^[jkhlwbeWBE^$0*#nN+\-;,GHLM()]$'
\ || char == "\<BS>" || char == "\<C-Y>" || char == "\<C-U>"
\ || char == "\<C-O>" || char == "\<C-^>"
" One char commands
if a:key == 9 " <TAB>
let char = '1'.char
endif
call s:DoNormal(char)
elseif char =~# '^[ p]$' " scroll forward/backward
call s:DoNormal(nr2char(2 * (char == ' ' ? 3 : 1)))
elseif s:Is2KeyCmd(char)
" Commands which take a second char
call s:HandleMultiKey(char, 0)
elseif (char + 0) || char == "\<C-W>"
" Accept things like "10G", "<C-W>2k"
call s:HandleMultiKey(char, 1)
elseif char == "\<CR>" || char == "\<C-CR>" || char == "\<S-CR>"
call s:HandleEnter(!(char == "\<CR>"))
elseif char == "\<C-N>" || char == "\<C-P>"
call s:HandleNextChanServ(char)
elseif char == ':'
if s:Execute(s:DoInput(0, ':', ''))
" Pause after commands which produce outputs
return s:PromptEnter(2)
endif
elseif char =~# '^[/?]$'
call s:SearchWord(char)
elseif char == "\<C-L>"
call s:RestoreWinSize(1)
elseif char =~# '^[ORo]$' && s:IsBufType_List()
if char ==# 'R'
call s:UpdateList()
else
call s:Sort{char ==# 'o' ? 'Select' : 'Reverse'}()
endif
elseif char =~? '^[AIO]$'
if s:IsBufType_Command() && char =~# '^[AIa]$'
" Position cursor at an appropriate place
if char ==# 'I'
call s:DoNormal('^')
endif
call s:DoInsert(char ==? 'a')
else
call s:OpenBuf_Command(1)
endif
call s:RedrawScreen(0)
throw 'IMGONNAPOST'
elseif char == "\<F1>"
call s:Cmd_HELP()
elseif char ==? 'r'
call s:ReconnServer(char ==# 'R')
elseif char ==? 'q'
call s:QuitWhat(char ==# 'Q')
endif
call s:HiliteBuffer()
" Accept repetitive inputs
let key = s:ConsumeKey()
if !s:IsZero(key)
return s:HandleKey(key)
endif
call s:SetLastActive()
endfunction
function! s:HandleMultiKey(char, multi)
let char = ''
let comd = a:char
let cnt = (a:char + 0)
let ctrl_w = s:IsCtrl_W(a:char)
let multi = a:multi
while 1
call s:ShowCmd(comd)
let char = s:GetChar(0)
if (char =~# '^[[:escape:]]\=$') || (multi && !cnt && s:IsZero(char))
let comd = ''
if s:IsZero(char)
call s:Beep(1)
endif
elseif char == "\<Del>"
let comd = (comd =~ '\d$') ? substitute(comd, '.$', '', '') : ''
if strlen(comd)
continue
endif
else
let comd = comd.char
endif
" Continue if it is a number or <C-W>
if !(multi && strlen(comd))
break
endif
if (char >= '0' && char <= '9')
" `cnt' is a mere indicator which shows that user is currently typing
" count
let cnt = 1
else
if ctrl_w
break
elseif s:IsCtrl_W(char)
let cnt = 0
let ctrl_w = 1
elseif s:Is2KeyCmd(char)
let multi = 0
else
break
endif
endif
endwhile
call s:UnstickKey(0)
if !strlen(comd)
return
endif
if char == "\<CR>" || char == "\<C-CR>" || char == "\<S-CR>"
call s:HandleEnter((ctrl_w || char != "\<CR>"), s:ComputeCount(comd))
elseif char =~# '^[]$' || (ctrl_w && char =~? '^[NP]$')
" XXX: Overriding Vim's <C-W>p, which isn't very useful because of
" redrawing of nicks and info-bar windows.
call s:HandleNextChanServ(comd)
elseif comd =~ '@.$'
call s:HandleAt(comd)
elseif comd =~# 'Z[ZQ]$'
call s:QuitWhat(comd =~# 'Q$')
else
call s:DoNormal(comd, 1)
endif
endfunction
function! s:HandleNextChanServ(comd)
call s:OpenNextChanServ((a:comd =~? '[N]'), (a:comd =~# ''),
\ s:ComputeCount(a:comd))
endfunction
function! s:ComputeCount(comd)
let cnt = 1
let nums = s:StrCompress(substitute(a:comd, '\D\+', ' ', 'g'))
while strlen(nums)
let cnt = cnt * s:StrDivide(nums, 1)
let nums = s:StrDivide(nums, 2)
endwhile
return cnt
endfunction
" Make use of the key input for the 'Hit any key to continue' prompt
function! s:HandlePromptKey(timeout, mesg, ...)
let key = s:PromptKey((10 * a:timeout), a:mesg, (a:0 ? a:1 : 'Question'))
" Do nothing with space, <C-C> etc.
if s:Key2Char(key) =~# '^[ [:return:][:escape:]]\=$'
call s:HiliteBuffer()
else
try
call s:HandleKey(key)
catch /^\S\+:E/
call s:IgnoreException(v:exception)
endtry
endif
endfunction
function! s:SendLines() range
" TODO: Do confirmation before sending (preferably optionally)
if !s:IsBufType_Command()
return
endif
try
let cmdbuf = bufnr('%')
let destbuf= cmdbuf
let i = a:firstline
while 1
try
" Remember the context for a while in which the command/message was
" entered.
" NOTE: Current server/buffer may change each time after SendLine()
call s:SetCurServer(getbufvar(cmdbuf, 'server'),
\ getbufvar(cmdbuf, 'channel'))
if i > a:lastline || !s:BufVisit(cmdbuf)
" NOTE: Do NOT put this in the `finally' clause: HandleKey() may
" want to reuse this command buffer
call s:PreCloseBuf_Command(destbuf, (a:firstline == a:lastline))
break
endif
" Get the destination buffer
let destbuf = s:SendLine(s:ExpandAlias(s:StrTrim(getline(i))))
" Prevent sending multiple lines too fast to the server
call s:DoWait(a:lastline, i)
catch /^SYNTAX ERROR/
call s:EchoError(v:exception)
endtry
let i = i + 1
endwhile
catch /^IMGONNA/
if s:in_loop
throw v:exception
endif
return
finally
unlet! s:channel
endtry
call s:SetLastActive()
call s:MainLoop()
endfunction
function! s:SendLine(line)
if strlen(a:line)
let comd = ''
let args = a:line
let rx = s:GetCmdRx(comd)
if a:line =~ rx
let s:split = strlen(s:StrMatch(a:line, rx, '\1'))
let comd = s:ExpandCmd(s:StrMatch(a:line, rx, '\2'))
let args = s:ExpandArgs(comd, s:StrMatch(a:line, rx, '\3'))
if strlen(args)
" This provision is only for removing a leading colon which user
" unnecessarily appended to the message and adding it back! Silly and
" redundant
let rx = s:GetCmdRx(comd)
" NOTE: Matching with empty string always results in true (!!)
if strlen(rx) && args =~ rx
let args = s:StrMatch(args, rx, '\1').s:StrMatch(args, rx, ' :\2')
endif
let args = s:StrTrim(args)
endif
endif
" Values will be copied several times by value, which I'm pretending
" I don't mind now: I just felt like making this function look simple.
if s:PreDoSend(comd, args)
call s:PostDoSend(comd, strlen(args))
endif
let s:split = 0
endif
return bufnr('%')
endfunction
function! s:PreSendMSG(mesg)
let send = 0
let mesg = substitute(a:mesg, '^/\s*', '', '')
if strlen(mesg)
let send = strlen(s:channel)
if send
call s:SendMSG('PRIVMSG', s:channel.' :'.mesg)
else
call s:EchoError('You are not on a channel.')
endif
endif
return send
endfunction
function! s:PreDoSend(comd, args)
let send = exists('*s:Cmd_{a:comd}')
if send
call s:Cmd_{a:comd}(a:args)
else
let send = (s:IsConnected() || a:comd =~# '^G\%(QUIT\|AWAY\)$')
if send
if !strlen(a:comd)
let send = s:PreSendMSG(a:args)
elseif exists('*s:Send_{a:comd}')
call s:Send_{a:comd}(a:comd, a:args)
else
call s:DoSend(a:comd, a:args)
endif
else
call s:EchoWarn('Do /SERVER first to get connected')
endif
endif
return send
endfunction
" Move cursor to an appropriate window
function! s:PostDoSend(comd, arglen)
if a:comd =~# '^\%(JOIN\|LIST\|NOTICE\|PRIVMSG\)$'
return
endif
if s:IsChanChat(s:channel) && a:comd !~# '^\%(INFO\|MOTD\|NAMES\|WHO\)$'
" If user entered the command from a channel, visit there
call s:VisitBuf_ChanChat(s:channel)
" Scroll to the bottom only if user is in talkative mood
if a:comd !~# '^\%(ACTION\)\=$'
return
endif
else
" When receiving a bunch of lines, visit the server window beforehand:
" avoid flicker caused by cursor movement between server and channel
" windows
if a:comd =~# '^\%(NAMES\)$' && a:arglen
" The message will be quite short. O.K. to exit here
return
endif
call s:VisitBuf_Server()
endif
if s:IsBufType_ChanServ()
" Prepare receiving lines
call s:WinLine('$')
endif
endfunction
function! s:SetLastActive()
if !s:autoaway || s:lastactive >= localtime()
return
endif
let lastactive = s:lastactive
let s:lastactive = localtime()
" Clear the `away' status only if considered to be set...
if s:lastactive >= lastactive + s:autoawaytime
" since I don't want to call this perl code every time you type something
call s:Send_GAWAY('AWAY', '')
endif
endfunction
function! s:SelectNickAction() range
if !(s:IsBufType_Nicks() && s:IsConnected(b:server))
return
endif
let nicks = ''
let i = a:firstline
while i <= a:lastline
let nick = substitute(getline(i), '^[@+]\+', '', '')
if strlen(nick)
let nicks = nicks.(strlen(nicks) ? ' ' : '').nick
endif
let i = i + 1
endwhile
let number= a:lastline - a:firstline + 1
let comds = "&whois\n&query\n&control\n&dcc/ctcp"
let choice = s:Confirm('What do you do with '.nicks.'?', comds)
if !choice
return
endif
if !(choice % 2) && number > 1
" TODO: Do it.
return s:EchoError('Sorry, you cannot do it with mulitple persons at '.
\'the same time')
endif
if (choice == 2 || choice == 3) && !s:IsChannel(b:channel)
return s:EchoError('You cannot do it unless you are on a channel.')
endif
" Set the current server appropriately, so the command will be sent to the
" one user intended
call s:SetCurServer(b:server, b:channel)
try
if choice == 1
call s:DoSend('WHOIS', substitute(nicks, ' ', ',', 'g'))
elseif choice == 2
call s:StartChat(nicks)
elseif choice == 3
call s:SelectNickOpVoice(nicks)
elseif choice == 4
call s:SelectNickCTCP(nicks)
endif
catch /^IMGONNA/
if s:in_loop
throw v:exception
endif
return
finally
unlet! s:channel
endtry
if !s:in_loop
call s:SetLastActive()
endif
endfunction
function! s:SelectNickOpVoice(nicks)
let choice = s:Confirm('Choose one of these:',
\ "&1 Op\n&2 Deop\n&3 Voice\n&4 Devoice")
if choice
let onoff = (choice % 2) ? '+' : '-'
let mode = choice >= 3 ? 'v' : 'o'
call s:SetOpVoice(onoff, mode, a:nicks)
endif
endfunction
function! s:SelectNickCTCP(nicks)
let choice = s:Confirm('Choose one of these:',
\ "&send\n&chat\n&ping\n&time\n&version\nclient&info")
if choice
let type = 'CTCP'
let args = ''
if choice >= 3
if choice == 3
let args = 'ping'
elseif choice == 4
let args = 'time'
elseif choice == 5
let args = 'version'
elseif choice == 6
let args = 'clientinfo'
endif
let args = a:nicks.' '.args
else
let type = 'DCC'
let args = (choice == 1 ? 'send' : 'chat').' '.a:nicks
endif
call s:Send_{type}(type, args)
endif
endfunction
"
" Selecting sort method
"
function! s:SortSelect()
if !s:IsBufType_List() || s:GetVimVar('b:updating')
return
endif
let choice = s:Confirm("Sort by what?", "&channel\n&member\n&topic")
if !choice
return
endif
if choice == 1
let cmp = 'channel'
elseif choice == 2
let cmp = 'member'
elseif choice == 3
let cmp = 'topic'
endif
let orgln = getline('.')
call s:ModifyBuf(1)
call s:SortList(cmp, (s:GetVimVar('b:sortdir') + 0))
call s:ModifyBuf(0)
call s:SearchLine(orgln)
endfunction
function! s:SortReverse()
if !s:IsBufType_List() || s:GetVimVar('b:updating')
return
endif
let orgln = line('.')
call s:ModifyBuf(1)
call s:BufReverse()
call s:ModifyBuf(0)
let b:sortdir = !(s:GetVimVar('b:sortdir'))
execute (line('$') - orgln + 1)
endfunction
"
" Controlling VimIRC options
"
function! s:OptList(list)
let list = a:list
call s:EchoHL('Current setting'.(list =~ ' ' ? 's' : '').':', 'Title')
while strlen(list)
let opt = s:StrDivide(list, 1)
echo opt.':' s:GetVimVar('s:'.opt)
let list = s:StrDivide(list, 2)
endwhile
call s:PromptEnter(3)
endfunction
" Internalize user variables (for safety)
function! s:OptSet(opt, ...)
let opt = substitute(a:opt, '^vimirc_', '', '')
let s:{opt} = s:GetVimVar('g:'.a:opt)
unlet! g:{a:opt}
" If it wasn't defined by the user, set it
if !strlen(s:{opt}) && a:0
let s:{opt} = a:1
endif
call s:OptValidate(opt)
endfunction
function! s:OptValidate(opt)
let var = 's:{a:opt}'
" Fix irregularities if encountered
if a:opt =~# '^\%(autoaway\|autoawaytime\|cmdheight\|dccport\|infowidth\|listexpire\|log\|nickswidth\)$'
" Make sure the value is a valid numeral
let {var} = {var} + 0
if a:opt ==# 'dccport'
" Avoid well-known ports
if {var} <= 1023
let {var} = 1024
endif
elseif a:opt =~# '\%(height\|width\)$'
if {var} <= 0 " not acceptable
let {var} = 1
endif
call s:RestoreWinSize(1)
endif
else
let {var} = s:StrCompress({var})
if a:opt ==# 'partmsg'
" Prepend a leading colon
let {var} = substitute({var}, '^[^:]', ':&', '')
elseif a:opt ==# 'browser'
let {var} = substitute({var}, '^!', '', '')
elseif a:opt =~# '\%(dir\|file\)$'
let {var} = s:ValidatePath({var})
endif
endif
endfunction
" "/SET option value"
function! s:Cmd_SET(optval, ...)
let unset = (a:0 && a:1)
" Settable options
let numopt = 'autoaway autoawaytime browser log dccport listexpire '.
\'infowidth nickswidth cmdheight'
let stropt = 'partmsg'
let rx = '^\(\S\+\)\%(\s\+\(.\+\)\)\=$'
let opt = tolower(s:StrMatch(a:optval, rx, '\1'))
let val = s:StrMatch(a:optval, rx, '\2')
if strlen(opt)
\ && ( opt =~# '^\%('.substitute(numopt, ' ', '\\|', 'g').'\)$'
\ || opt =~# '^\%('.substitute(stropt, ' ', '\\|', 'g').'\)$')
if strlen(val) || unset
let s:{opt} = val
call s:OptValidate(opt)
endif
call s:OptList(opt)
else
call s:OptList(numopt.' '.stropt)
endif
endfunction
function! s:Cmd_UNSET(opt)
call s:Cmd_SET(a:opt, 1)
endfunction
"
" Controlling user privileges
"
function! s:SetOpVoice(onoff, mode, args)
let channel = s:StrDivide(a:args, 1)
let nicks = s:StrDivide(a:args, 2)
if !s:IsChannel(channel)
let channel = s:channel
let nicks = a:args
endif
if !(s:IsChannel(channel) && strlen(nicks))
return s:EchoError('Syntax: /op [channel] nick(s)')
endif
let nicks = s:StrCompress(substitute(nicks, ',', ' ', 'g'))
let number= strlen(substitute(nicks, '\S\+', '', 'g')) + 1
let modes = s:StrMultiply(a:mode, number)
call s:DoSend('MODE', channel.' '.a:onoff.modes.' '.nicks)
endfunction
function! s:Cmd_OP(args)
call s:SetOpVoice('+', 'o', a:args)
endfunction
function! s:Cmd_DEOP(args)
call s:SetOpVoice('-', 'o', a:args)
endfunction
function! s:Cmd_VOICE(args)
call s:SetOpVoice('+', 'v', a:args)
endfunction
function! s:Cmd_DEVOICE(args)
call s:SetOpVoice('-', 'v', a:args)
endfunction
"
" Aliasing
"
function! s:ExpandAlias(line)
let line = a:line
let rx = s:GetCmdRx('')
if a:line =~ rx
let varname = s:RC_Varname('alias', toupper(s:StrMatch(a:line, rx, '\2')))
if exists('g:{varname}')
let args = s:StrMatch(a:line, rx, '\3')
let line = g:{varname}.(strlen(args) ? ' '.args : '')
if strlen(s:StrMatch(a:line, rx, '\1')) && match(line, '/\csplit')
let line = '/SPLIT '.line
endif
endif
endif
return line
endfunction
function! s:Cmd_ALIAS(line)
let rx = '^/\=\(\S\+\)\s\+/\=\(.\+\)$'
if a:line =~ rx
if s:RC_Open(1)
let alias = toupper(s:StrMatch(a:line, rx, '\1'))
let comd = s:StrMatch(a:line, rx, '/\2')
call s:RC_Section('Aliases')
call s:RC_Set('alias', alias, comd)
else
call s:EchoError('Failed in registering alias')
endif
call s:RC_Close()
else
call s:EchoError('Syntax: /ALIAS alias command (with arguments)')
endif
endfunction
function! s:Cmd_UNALIAS(alias)
if s:RC_Open(0)
call s:RC_Unset('alias', toupper(substitute(a:alias, '^/', '', '')))
endif
call s:RC_Close()
endfunction
" Expand command aliases (after s:ExpandAlias())
function! s:ExpandCmd(comd)
" NOTE: Beware of collisions
let comd = toupper(a:comd)
if comd =~# '^\%(B\%[YE]\|E\%[XIT]\|SI\%[GNOFF]\)$'
let comd = 'QUIT'
elseif comd =~# '^\%(CH\%[AT]\)$'
let comd = 'QUERY'
elseif comd =~# '^DA\%[TE]$'
let comd = 'TIME'
elseif comd =~# '^\%(LE\%[AVE]\)$'
let comd = 'PART'
elseif comd =~# '^\%(ME\)$'
let comd = 'ACTION'
elseif comd =~# '^\%(M\%[SG]\)$'
let comd = 'PRIVMSG'
elseif comd =~# '^\%(NICKS\)$'
let comd = 'NAMES'
elseif comd =~# '^\%(\%(RE\)\=CO\%[NNECT]\)$'
let comd = 'SERVER'
else
" Handle abbreviations
if comd =~# '^AC\%[TIO]$'
let comd = 'ACTION'
elseif comd =~# '^AL\%[IA]$'
let comd = 'ALIAS'
elseif comd =~# '^AWA\=$'
let comd = 'AWAY'
elseif comd =~# '^CTC\=$'
let comd = 'CTCP'
elseif comd =~# '^DC$'
let comd = 'DCC'
elseif comd =~# '^DEO$'
let comd = 'DEOP'
elseif comd =~# '^DES\%[CRIB]$'
let comd = 'DESCRIBE'
elseif comd =~# '^DEV\%[OIC]$'
let comd = 'DEVOICE'
elseif comd =~# '^GA\%[WA]$'
let comd = 'GAWAY'
elseif comd =~# '^GQ\%[UI]$'
let comd = 'GQUIT'
elseif comd =~# '^H\%[EL]$'
let comd = 'HELP'
elseif comd =~# '^INF\=$'
let comd = 'INFO'
elseif comd =~# '^J\%[OI]$'
let comd = 'JOIN'
elseif comd =~# '^L\%[IS]$'
let comd = 'LIST'
elseif comd =~# '^MOD\=$'
let comd = 'MODE'
elseif comd =~# '^MOT$'
let comd = 'MOTD'
elseif comd =~# '^NA\%[ME]$'
let comd = 'NAMES'
elseif comd =~# '^NIC\=$'
let comd = 'NICK'
elseif comd =~# '^NO\%[TIC]$'
let comd = 'NOTICE'
elseif comd =~# '^O$'
let comd = 'OP'
elseif comd =~# '^PAR\=$'
let comd = 'PART'
elseif comd =~# '^PIN\=$'
let comd = 'PING'
elseif comd =~# '^PR\%[IVMS]$'
let comd = 'PRIVMSG'
elseif comd =~# '^Q\%[UER]$'
let comd = 'QUERY'
elseif comd =~# '^QUI$'
let comd = 'QUIT'
elseif comd =~# '^S\%[ERVE]$'
let comd = 'SERVER'
"elseif comd =~# '^SET$'
elseif comd =~# '^T\%[IM]$'
let comd = 'TIME'
elseif comd =~# '^TO\%[PI]$'
let comd = 'TOPIC'
elseif comd =~# '^UNA\%[LIA]$'
let comd = 'UNALIAS'
elseif comd =~# '^UNSE\=$'
let comd = 'UNSET'
elseif comd =~# '^V\%[OIC]$'
let comd = 'VOICE'
elseif comd =~# '^WH$'
let comd = 'WHO'
elseif comd =~# '^WHOAM\=$'
let comd = 'WHOAMI'
elseif comd =~# '^WHOI$'
let comd = 'WHOIS'
endif
endif
return comd
endfunction
" Expand '%' to the current channel, etc.
" TODO: How should we behave to syntax errors?
function! s:ExpandArgs(comd, args)
"let errmsg = 'SYNTAX ERROR '
let args = a:args
if a:comd =~# '^\%(MODE\)$'
if a:args =~ '^\%([-+].*\)\=$'
let args = (s:IsChannel(s:channel) ? s:channel
\ : s:GetCurNick()).' '.a:args
endif
elseif a:comd =~# '^\%(NAMES\)$'
" Syntax: CHANNEL [<comma> CHANNEL]*
" User might delimit targets with spaces, which is wrong
let args = s:ExpandChannel(substitute(a:args, '\s\+', ',', 'g'))
elseif a:comd =~# '^WHOIS$'
" Syntax: [SERVER] USER [<comma> USER]*
let server= s:ExpandChannel(s:StrDivide(a:args, 1))
let nicks = s:ExpandChannel(substitute(s:StrDivide(a:args, 2),
\ '\s\+', ',', 'g'))
let args = s:StrTrim(server.(s:IsNick(server) ? ',' : ' ').nicks)
elseif a:comd =~# '^WHOWAS$'
" Syntax: USER [COUNT [SERVER]]
" NOTE: Some servers accept multiple users, delimited with commas, whereas
" RFC1459 specifies only one user. I adopt the latter.
if 1 || a:args =~ '^\S\+\%(\s\+-\=\d\+\%(\s\+\S\+\)\=\)\=$'
let args = s:ExpandChannel(a:args, ' ')
endif
elseif a:comd =~# '^\%(ISON\|USERHOST\)$'
" Syntax: USER [<space> USER]*
" User might delimit targets with commas, which is wrong
let args = s:ExpandChannel(substitute(a:args, ',', ' ', 'g'), ' ')
elseif a:comd =~# '^\%(INVITE\)$'
" Syntax: USER CHANNEL
let rx = '^\(\S\+\)\%(\s\+\(\S\+\)\)'.(s:IsChannel(s:channel)
\ ? '\=' : '').'$'
if a:args =~ rx
let nick = s:StrMatch(a:args, rx, '\1')
let channel = s:StrMatch(a:args, rx, '\2')
let args = nick.' '.{s:IsChannel(channel) ? 'l' : 's'}:channel
endif
elseif !s:IsCmdUnary(a:comd)
if a:comd =~# '^\%(ACTION\)$'
" NOTE: "/ME" command MUST come with NO targets specified
if strlen(s:channel)
let args = s:channel.' '.a:args
endif
elseif ( a:comd =~# '^\%(DESCRIBE\|NOTICE\|PART\|PRIVMSG\|TOPIC\)$'
\|| a:comd =~# '^\%(KICK\)$')
" Syntax: TARGET [MESSAGE]
" CHANNEL USER [MESSAGE]
" Divide arguments into two parts, to see if the former is righteously
" a channel or not
let rx = '^\(\S\+\)\%(\s\+\(.\+\)\)\=$'
let channel = s:ExpandChannel(s:StrMatch(a:args, rx, '\1'))
let message = s:StrMatch(a:args, rx, '\2')
if !s:IsChannel(channel) && a:comd =~# '^\%(KICK\|PART\|TOPIC\)$'
" If user ommitted channel(s), supply the current one
let channel = s:channel
let message = a:args
endif
" Restore it, potentially squeezing the spaces in-between
if strlen(channel)
let args = channel.' '.message
endif
endif
endif
return args
endfunction
function! s:ExpandChannel(channel, ...)
let delimit = (a:0 && strlen(a:1)) ? a:1 : ','
let channel = substitute(a:channel, '%', s:channel, '')
let channel = substitute(channel, '%', '', 'g')
let channel = s:StrCompress(channel, delimit)
return channel
endfunction
" Syntax: COMMAND [MESSAGE]
function! s:IsCmdUnary(comd)
return (a:comd =~# '^\%(G\=\%(AWAY\|QUIT\)\|WALLOPS\)$')
endfunction
" Syntax: COMMAND TARGET [MESSAGE]
function! s:IsCmdBinary(comd)
return (a:comd =~#
\'^\%(ACTION\|DESCRIBE\|KILL\|NOTICE\|PART\|PRIVMSG\|TOPIC\|SQU\%(ERY\|IT\)\)$')
endfunction
" Syntax: COMMAND CHANNEL USER [MESSAGE]
function! s:IsCmdTernary(comd)
return (a:comd =~# '^\%(KICK\)$')
endfunction
function! s:GetCmdRx(comd)
let rx = ''
if !strlen(a:comd)
" Pattern of the command line
let rx = '^/\(\csp\%[lit]\s\+/\=\)\=\(\S\+\)\%(\s\+\(.\+\)\)\=$'
elseif s:IsCmdUnary(a:comd)
let rx = '^\(\):\=\(.\+\)$'
elseif s:IsCmdBinary(a:comd)
let rx = '^\(\S\+\)\s\+:\=\(.\+\)$'
elseif s:IsCmdTernary(a:comd)
let rx = '^\(\S\+\s\+\S\+\)\s\+:\=\(.\+\)$'
endif
return rx
endfunction
"
" Wrapper functions for commencing communication
"
function! s:ReconnServer(force)
let server = s:GetVimVar('b:server')
if !s:IsServer(server)
let server = s:server
endif
if !s:IsConnected(server) && (a:force
\ || s:Confirm_YN('Reconnect to server '.server))
call s:Cmd_SERVER(server)
call s:MainLoop()
endif
endfunction
function! s:StartServer(...)
let manual = a:0
let list = manual ? a:1 : s:server
let retval = (strlen(list)
\ && (!manual || s:Confirm_YN('Open '.list.' with VimIRC')))
if retval
" irc.server.com/#channel
let rx = '^\%(irc://\)\=\(..\{-\}\)\%([/:]\(\S\+\)\)\=$'
" NOTE: The above style of channel specification is not allowed for
" `g:vimirc_server'. Use `g:vimirc_autojoin' instead.
while strlen(list)
let server = s:StrDivide(list, 1)
if strlen(server)
let channel= manual ? s:StrMatch(server, rx, '\2') : ''
let server = s:StrMatch(server, rx, '\1')
if manual
call s:HiliteURL(server)
if strlen(channel)
" I encountered this notation, unfortunately (missing #):
" irc://irc.efnet.net/hydrairc
let channel = (s:IsChannel(channel) ? '' : '#').channel
if s:StartChannel(channel, server) > 0 " already logged-in
return retval
endif
endif
endif
call s:Cmd_SERVER(server)
endif
let list = s:StrDivide(list, 2)
endwhile
endif
return retval
endfunction
function! s:StartChannel(channel, ...)
let server = a:0 ? a:1 : s:GetVimVar('b:server')
let retval = (s:IsServer(server) && s:Confirm_YN('Join channel '.a:channel))
if retval
if !s:IsBufType_List()
call s:HiliteURL(a:channel)
endif
if s:IsConnected(server)
call s:SetCurServer(server)
call s:Send_JOIN('JOIN', a:channel)
else
" Open specified channel after logging in the server
" FIXME: This can be overridden if called before the previous login
" attempt completes
let s:autojoin = a:channel
let retval = -1
endif
endif
return retval
endfunction
" TODO: Keep track of the state of the nick you are chatting with (ison, nick
" change)
function! s:StartChat(nick, ...)
" Open up a separate window as with DCC CHAT
" TODO: Accept multiple nicks
if s:IsNick(a:nick)
call s:OpenBuf_Chat(a:nick, (a:0 ? a:1 : s:server))
call s:OpenBuf_Command(1)
throw 'IMGONNAPOST'
elseif strlen(a:nick)
call s:EchoError('Not a proper nick: '.a:nick)
endif
endfunction
function! s:StartWeb(url)
let comd = (s:IsServer(a:url)
\ && s:Confirm_YN('Open '.a:url.' with web browser'))
\ ? s:GetUserBrowser() : ''
let retval = strlen(comd)
if retval
" "irc..." server not opened with VimIRC
let url = (a:url !~ '^\a\+://' ? 'http://' : '').a:url
call s:HiliteURL(url)
let url = s:StrQuote(s:EscapeFName(url))
if comd =~# '%URL%'
" Avoid special chars to be replaced with the matched pattern
let comd = substitute(comd, '%URL%', escape(url, '&\'), 'g')
else
let comd = comd.' '.url
endif
call s:ExecuteShell(comd)
endif
return retval
endfunction
function! s:Cmd_QUERY(args)
if !strlen(a:args)
call s:QuitChat(s:channel)
else
call s:StartChat(a:args)
endif
endfunction
function! s:UpdateList()
if s:IsBufType_List() && s:IsConnected(b:server)
" Prevent excessive updating
if (localtime() - b:updated) > 60
call s:SetCurServer(b:server)
call s:UnloadList()
call s:Send_LIST('LIST', '')
else
call s:EchoWarn('You are too eager to update. Wait another minute.')
endif
call s:MainLoop()
endif
endfunction
function! s:DoAutoJoin()
let autojoin = s:GetServerOpt(s:GetVimVar('g:vimirc_autojoin'), s:server)
while strlen(autojoin)
let channel = s:StrDivide(autojoin, 1)
if channel ==? 'list'
if s:GetBufNum_List() < 0
call s:Send_LIST('LIST', '')
endif
else
call s:Send_JOIN('JOIN', substitute(channel, ':', ' ', ''))
endif
let autojoin = s:StrDivide(autojoin, 2)
endwhile
if exists('s:autojoin')
call s:Send_JOIN('JOIN', s:autojoin)
unlet s:autojoin
endif
endfunction
"
" Opening URLs with web browser
" (heavily borrowed from Chalice)
"
function! s:ExtractChannel(str)
return matchstr(a:str, '\%(^\|\W\)\@<=[&#+!][^,:[:space:]]\+')
endfunction
function! s:ExtractURL(str)
" FIXME: Filenames would likely be regarded as URLs
" Catch raw IPs?
let url = matchstr(a:str,
\ '\%(\a\+://\)\=\%(\w[-.[:alnum:]]\+\.\)\+\a\{2,\}\%(/\S*\)\=')
if strlen(url)
let url = s:ValidateURL(url)
endif
return url
endfunction
function! s:ExtractLink()
let link = ''
let line = getline('.')
if line =~ '^\S\+ \*:'
return link
endif
let word = expand('<cWORD>')
let link = s:ExtractChannel(word)
if !strlen(link)
" Get URL under/after/before cursor
let link = s:ExtractURL(word)
if !strlen(link)
let link = s:ExtractURL(strpart(line, col('.')))
if !strlen(link)
let link = s:ExtractURL(line)
if !strlen(link)
let link = s:ExtractChannel(line)
endif
endif
endif
endif
return link
endfunction
function! s:OpenLink(split, ...)
let link = s:ExtractLink()
let open = strlen(link)
if open
let s:split = a:split
let open = s:IsChannel(link) ? s:StartChannel(link)
\ : (s:IsServerIRC(link) && s:StartServer(link)
\ || s:StartWeb(link))
let s:split = 0
endif
if !(open || s:IsBufType_List())
" Advance cursor if user selected nothing: this is called via <CR> key
call s:DoNormal((a:0 ? a:1 : v:count1)."\<CR>")
endif
endfunction
"
" Logging
"
function! s:LogBuffer(bufnum)
if s:log && s:MakeDir(s:logdir) && bufloaded(a:bufnum)
\ && (s:BufVisit(a:bufnum) || s:OpenBufNum('split', a:bufnum))
\ && !(s:IsBufEmpty() || (s:GetVimVar('b:lastsave') >= line('$')))
let save_cpoptions = &cpoptions
set cpoptions-=A
let range = ((exists('b:lastsave') ? b:lastsave : 0) + 1).',$'
let logfile = s:logdir.'/'.s:GenFName_Log()
" If the file is loaded in another buffer, unload it
call s:BufUnload(logfile)
execute 'redir >>' logfile
silent echo '(Logged at' s:GetTime(0).")\n"
redir END
call s:ExecuteShell(range.'write! >> '.logfile)
let b:lastsave = line('$')
let &cpoptions = save_cpoptions
endif
endfunction
function! s:GenFName_Log()
" I'm prepending your nick to avoid corrupted data in case you're running
" several instances. No locking.
return s:EscapeFName(s:GetCurNick().'@'.b:server.(exists('b:channel')
\ ? '.'.b:channel
\ : ''))
endfunction
"
" Caching
"
function! s:GenFName_List()
return s:logdir.'/'.b:server.'.list'
endfunction
function! s:CacheList(bufnum)
if (getbufvar(a:bufnum, 'updated') + 0 > 0) && s:MakeDir(s:logdir)
\ && (s:BufVisit(a:bufnum) || s:OpenBufNum('split', a:bufnum))
call s:Write(s:GenFName_List(), 0)
let b:updated = 0
endif
endfunction
function! s:LoadList()
call s:OpenBuf_List()
if s:IsBufEmpty()
let list = s:GenFName_List()
if filereadable(list) && (localtime() - getftime(list)) < s:listexpire
call s:Read(list)
endif
endif
return !s:IsBufEmpty()
endfunction
function! s:UnloadList()
let b:updated = localtime()
let b:updating= 1
call s:ModifyBuf(1)
call s:BufClear()
call s:ModifyBuf(0)
call delete(s:GenFName_List())
endfunction
"
" Notifications
"
function! s:NotifyNewEntry(force)
" If the bottom line is already visible, or just forced to do so,
if a:force || s:IsBottomVisible(1)
" Scroll down
call s:HiliteLine('$')
else
" And if not, do not scroll. User might want to stay there to read old
" messages
call s:Beep(1)
endif
endfunction
function! s:NotifyOffline()
call s:HiliteClear()
if s:IsBufType_IRC()
echo s:IsSockOpen() ? s:IsBufType_Command()
\ ? 'Hitting <CR> will send out the current line'
\ : 'Hit <Space> to get online'
\ : 'Do /SERVER to get connected'
endif
if s:in_loop
if s:debug
call s:EchoHL('You have to consider seriously why you are seeing this '
\'message', 'WarningMsg')
endif
let s:in_loop = 0
endif
try
call s:DoTimer()
catch /^Vim:Interrupt$/
endtry
endfunction
function! s:GetBufTitle()
return exists('b:title') ? b:title : bufname('%')
endfunction
function! s:SetTitleBar(time)
let &titlestring = s:client." [".strftime('%H:%M', a:time).']: '.
\(s:IsBufType_IRC()
\ ? b:server.' '.(s:IsBufType_Channel()
\ ? b:channel.': '.b:topic : '')
\ : fnamemodify(expand('%'), ':p'))
" NOTE: Redrawing is necessary to update the title
redraw
endfunction
function! s:SetUserMode(umode)
let bufnum = s:GetBufNum_Server()
if bufnum >= 0
let umode = (a:umode == '+' ? '' : a:umode)
call setbufvar(bufnum, 'umode', umode)
call setbufvar(bufnum, 'title', ' '.s:GetCurNick().
\(strlen(umode) ? " [".umode.']' : '').' @ '.s:server)
endif
endfunction
function! s:SetChannelTopic(channel, topic)
let bufnum = s:GetBufNum_Channel(a:channel)
if bufnum >= 0
call setbufvar(bufnum, 'topic', a:topic)
endif
endfunction
function! s:SetChannelMode(channel, cmode)
let bufnum = s:GetBufNum_Channel(a:channel)
if bufnum >= 0
call setbufvar(bufnum, 'cmode', a:cmode)
call setbufvar(bufnum, 'title', ' '.a:channel." [".a:cmode.'] @ '.s:server)
endif
endfunction
"
" Help
"
function! s:Cmd_HELP(...)
if a:0 && strlen(a:1)
if a:1 ==? 'DCC'
call s:Cmd_DCCHELP()
elseif a:1 =~? '^\%(SERVER\|REMOTE\)\>'
call s:Cmd_REMOTEHELP(s:StrDivide(a:1, 2))
endif
return
endif
try
let save_more = &more
set more
let hlname = 'Title'
call s:EchoHL(' VimIRC Help', hlname)
echo "\n"
call s:EchoHL(' Available commands:', hlname)
call s:EchoHL(' (letters in [] are optional)', 'Comment')
echo "\n"
call s:EchoHL('/s[erver] [host[:port] [password]]', hlname)
echo "\tTry to connect with a new server. Or reconnect the current server"
echo "\twhen no argument is given."
echo "\tSynonym: co[nnect]"
call s:EchoHL('/qui[t] [reason]', hlname)
echo "\tDisconnect with the current server."
echo "\tSynonyms: b[ye], e[xit], si[gnoff]."
call s:EchoHL('/gq[uit] [reason]', hlname)
echo "\tDisconnect with all the servers."
echo "\n"
call s:EchoHL('/l[ist]', hlname)
echo "\tShow the list of active channels on the server."
echo "\n"
call s:EchoHL('/j[oin] channel(s) [key(s)]', hlname)
echo "\tJoin specified channels. Use commas to separate channels."
call s:EchoHL('/pa[rt] [channel(s)] [message]', hlname)
echo "\tExit from the specified channels. If channels are ommitted, exit"
echo "\tfrom the current channel."
echo "\tSynonym: le[ave]."
echo "\n"
call s:EchoHL('/to[pic] [channel] [topic]', hlname)
echo "\tSet or show the current topic for channel."
echo "\n"
call s:EchoHL('/q[uery] [nick]', hlname)
echo "\tWith nick, start a query session with the user. Without nick,"
echo "\tclose the current query session."
echo "\tSynonym: ch[at]"
echo "\n"
call s:EchoHL('message', hlname)
echo "\tSend a message to the current channel, or the nick currently"
echo "\tchatting with."
echo "\n"
call s:EchoHL('/m[sg] target message', hlname)
echo "\tSend a message to the nick/channel."
call s:EchoHL('', hlname)
echo "\n"
call s:EchoHL('/me message', hlname)
echo "\tSend a message to the current channel/query target, playing some"
echo "\trole."
call s:EchoHL('/des[cribe] target message', hlname)
echo "\tSend a message to the nick/channel, playing some role."
echo "\n"
call s:EchoHL('/t[ime]', hlname)
echo "\tShow what time it is on the server now."
echo "\tSynonym: da[te]"
echo "\n"
call s:EchoHL('/aw[ay] [reason]', hlname)
echo "\tWith reason, notify the server that you are away. Without reason,"
echo "\tnotify her that you are back."
call s:EchoHL('/ga[way] [reason]', hlname)
echo "\tDo the same thing with all servers."
echo "\n"
call s:EchoHL('/whoa[mi]', hlname)
echo "\tShow the current nickname of you."
call s:EchoHL('/whoi[s] [server] nick(s)', hlname)
echo "\tShow information on nicks, separated by commas."
echo "\n"
call s:EchoHL('/o[p] [channel] nick(s)', hlname)
call s:EchoHL('/v[oice] [channel] nick(s)', hlname)
echo "\tGive operator or voice privileges to the selected nick(s)."
echo "\tUse spaces or commas to separate them."
call s:EchoHL('/deo[p] [channel] nick(s)', hlname)
call s:EchoHL('/dev[oice] [channel] nick(s)', hlname)
echo "\tDeprive operator or voice privileges of the selected nick(s)."
echo "\n"
call s:EchoHL('/set [option [value]]', hlname)
echo "\tSet or show internal option values. List of settable options will"
echo "\tbe displayed if option is ommited."
call s:EchoHL('/uns[et] option', hlname)
echo "\tClear the value of the option"
echo "\n"
call s:EchoHL('/al[ias] /new-command /blah blah', hlname)
echo "\tAdd a new command \"new-command\" which expands into \"blah blah\"."
call s:EchoHL('/una[lias] /new-command', hlname)
echo "\tRemove it."
echo "\n"
call s:EchoHL('/sp[lit]', hlname)
echo "\tThis is a prefix to commands such as /join, /list, /query, and"
echo "\t/server, to execute those commands in separate windows."
echo "\n"
call s:EchoHL('/h[elp]', hlname)
echo "\tDisplay this message."
call s:EchoHL('/dc[c] help', hlname)
echo "\tDisplay a help message for DCC commands."
echo "\n"
call s:PromptEnter(0)
catch /^Vim:Interrupt$/
finally
let &more = save_more
endtry
endfunction
function! s:Cmd_DCCHELP(...)
try
let save_more = &more
set more
let hlname = 'Title'
call s:EchoHL(' Available DCC commands:', hlname)
echo "\n"
call s:EchoHL('/dc[c] send [nick [file]]', hlname)
echo "\tOffer DCC SEND to nick"
call s:EchoHL('/dc[c] chat [nick]', hlname)
echo "\tOffer DCC CHAT to, or accept pending offer from, nick"
call s:EchoHL('/dc[c] get [nick [file]]', hlname)
echo "\tAccept pending SEND offer from nick"
call s:EchoHL('/dc[c] close [type [nick]]', hlname)
echo "\tClose SEND/CHAT/GET connection with nick"
call s:EchoHL('/dc[c] list', hlname)
echo "\tList all active/pending DCC connections"
echo "\n"
call s:PromptEnter(0)
catch /^Vim:Interrupt$/
finally
let &more = save_more
endtry
endfunction
function! s:Cmd_REMOTEHELP(args)
" XXX: Are there any servers who accept arguments?
call s:DoSend('HELP', a:args)
endfunction
function! s:Cmd_WHOAMI(...)
call s:EchoNormal('You are '.s:GetCurNick())
endfunction
"
" Misc. utility functions
"
" Generic ones
function! s:Beep(times)
if a:times <= 0 || s:lastbeep >= localtime() - 1
return
endif
try
let errorbells = &errorbells
let visualbell = &visualbell
set errorbells
set novisualbell
let i = 1
while i <= a:times
call s:DoNormal("\<Esc>")
call s:DoWait(a:times, i)
let i = i + 1
endwhile
finally
let &errorbells = errorbells
let &visualbell = visualbell
let s:lastbeep = localtime()
endtry
endfunction
function! s:ClearCommand(newline)
echon "\r" (a:newline ? "\n" : '')
endfunction
function! s:EchoHL(mesg, hlname)
try
execute 'echohl' a:hlname
echo a:mesg
catch /^Vim:Interrupt$/
finally
echohl None
endtry
endfunction
function! s:EchoError(mesg)
call s:Beep(1)
call s:HandlePromptKey(1, a:mesg, 'ErrorMsg')
endfunction
function! s:EchoNormal(mesg)
call s:HandlePromptKey(1, a:mesg, 'Normal')
endfunction
function! s:EchoWarn(mesg)
call s:HandlePromptKey(1, a:mesg, 'WarningMsg')
endfunction
function! s:Execute(comd, ...)
let retval = 0
if strlen(a:comd)
let silent = (a:0 && a:1)
try
let retval = s:Execute{silent ? 'Silent' : 'Loud'}(a:comd)
catch /^Vim:Interrupt$/
endtry
endif
return retval
endfunction
function! s:ExecuteLoud(comd)
let loud = 0
call s:UntoggleCursor(1)
try
let save_more = &more
let save_reg = @"
let @" = @_
set more
redir @"
execute a:comd
redir END
" XXX: Vim sometimes echoes just "\r\n"
let loud = (strlen(@") && @" !~ '^[\r\n]\+$')
if !loud && strlen(@")
call s:RedrawScreen(0)
endif
finally
let &more = save_more
let @" = save_reg
call s:UntoggleCursor(0)
endtry
return loud
endfunction
function! s:ExecuteSafe(prefix, comd)
execute (exists(':'.a:prefix) == 2 ? a:prefix.' ' : '').a:comd
endfunction
function! s:ExecuteShell(comd)
let comd = a:comd
if &shellxquote == '"' && s:IsWin3264() && !match(a:comd, '^start ')
" Remove unecessary escapes
let comd = substitute(comd, '\\"\@=', '', 'g')
endif
call s:ExecuteSilent('!'.comd)
endfunction
function! s:ExecuteSilent(comd)
let v:errmsg = ''
silent! execute a:comd
return !strlen(v:errmsg)
endfunction
function! s:DoInsert(append)
execute 'startinsert'.(a:append ? '!' : '')
endfunction
function! s:DoIterate(ex, comd, times)
let loud = 0
if strlen(a:comd)
let i = 1
while i <= a:times
if a:ex
let loud = s:ExecuteLoud(a:comd)
else
call s:DoNormal(a:comd, 1)
endif
let i = i + 1
endwhile
endif
return loud
endfunction
function! s:DoNormal(comd, ...)
if strlen(a:comd)
if !(a:0 && a:1)
execute 'normal!' a:comd
else
call s:DoNormalSafe(a:comd)
endif
endif
endfunction
" Executes normal mode commands. Do not try to make changes with this.
" NOTE: This can be slow if used repetitively
function! s:DoNormalSafe(comd)
try
let orgbuf = bufnr('%')
let modifiable = &l:modifiable
" Temporarily forbid user to tamper with the buffer.
call setbufvar(orgbuf, '&modifiable', 0)
execute 'normal!' a:comd
finally
call setbufvar(orgbuf, '&modifiable', modifiable)
endtry
endfunction
function! s:DoWincmd(comd, ...)
call s:ExecuteSilent((a:0 ? a:1 : '').'wincmd '.a:comd)
return winnr()
endfunction
function! s:GetEnv(var)
let var = expand(a:var)
if var ==# a:var
let var = ''
endif
return var
endfunction
function! s:GetTime(short, ...)
return strftime((a:short ? '%H:%M' : '%Y/%m/%d %H:%M:%S'),
\ (a:0 && a:1 ? a:1 : localtime()))
endfunction
function! s:GetVimVar(varname)
return exists('{a:varname}') ? {a:varname} : ''
endfunction
function! s:Read(file)
let save_cpoptions = &cpoptions
set cpoptions-=a
if !&l:modifiable
setlocal modifiable
endif
call s:ExecuteSilent('read '.a:file)
1call s:DelLine()
let &cpoptions = save_cpoptions
return !s:IsBufEmpty()
endfunction
function! s:RedrawScreen(all)
execute 'redraw'.(a:all ? '!' : '')
endfunction
function! s:RedrawStatus(...)
call s:ExecuteSilent('redrawstatus'.(a:0 && a:1 ? '!' : ''))
endfunction
" Show user that she is in pending-mode
function! s:ShowCmd(comd)
" Right-aligning the message
echon s:StrMultiply(' ', (&columns - 20))
\ strpart(a:comd, (strlen(a:comd) > 8 ? strlen(a:comd) - 8 : 0)) "\r"
endfunction
function! s:Write(file, append)
" TODO: Range support
if s:IsBufEmpty()
return
endif
let save_cpoptions = &cpoptions
set cpoptions-=A
" Cannot write if it is loaded elsewhere
call s:BufUnload(a:file)
call s:ExecuteSilent('write! '.(a:append ? '>> ' : '').a:file)
let &cpoptions = save_cpoptions
endfunction
" Interactive functions
function! s:Confirm(mesg, list)
let choice = 0
call s:UntoggleCursor(1)
try
let choice = confirm(a:mesg, a:list)
catch /^Vim:Interrupt$/
finally
call s:RedrawScreen(0)
call s:UntoggleCursor(0)
endtry
return choice
endfunction
function! s:Confirm_YN(mesg)
return (s:PromptChar(20, ' '.a:mesg.'? (y/[n]): ', 'Question') ==? 'y')
endfunction
function! s:Input(mesg, ...)
return s:DoInput(0, a:mesg.': ', (a:0 ? a:1 : ''))
endfunction
function! s:InputS(mesg)
return s:DoInput(1, a:mesg.': ', '')
endfunction
function! s:DoInput(secret, mesg, text)
let input = ''
call s:UntoggleCursor(1)
try
let input = s:StrCompress(input{a:secret ? 'secret' : ''}(a:mesg, a:text))
catch /^Vim:Interrupt$/
finally
call s:UntoggleCursor(0)
endtry
return input
endfunction
function! s:Key2Char(key)
" Special keys (<F1> etc.) are char values already (f_getchar() in eval.c)
return type(a:key) ? a:key : nr2char(a:key)
endfunction
function! s:GetChar(cursor)
return s:Key2Char(s:GetKey(a:cursor))
endfunction
function! s:GetKey(cursor)
let key = 0
if a:cursor
call s:UntoggleCursor(1)
endif
try
let key = getchar()
catch /^Vim:Interrupt$/
finally
call s:ClearCommand(0)
if a:cursor
call s:UntoggleCursor(0)
endif
endtry
return key
endfunction
function! s:PromptChar(timeout, mesg, ...)
return s:Key2Char(s:PromptKey(a:timeout, a:mesg, (a:0 ? a:1 : 'MoreMsg')))
endfunction
function! s:PromptEnter(timeout)
call s:ClearCommand(1)
call s:HandlePromptKey(a:timeout, 'Hit ENTER or type command to continue')
endfunction
function! s:PromptKey(timeout, mesg, ...)
" TODO: Make timeout-length configurable
let key = 0
let hlname = a:0 ? a:1 : 'MoreMsg'
call s:ClearCommand(0)
if a:timeout > 0
let key = s:PromptKeyTick(a:timeout, a:mesg, hlname)
else
call s:EchoHL(a:mesg, hlname)
let key = s:GetKey(1)
endif
call s:ClearCommand(0)
return key
endfunction
" PromptKey with timeout feature
function! s:PromptKeyTick(timeout, mesg, hlname)
let key = 0
try
let startT = localtime()
while (a:timeout - (s:DoTick(a:mesg, '\|/-', a:hlname) - startT)) > 0
if getchar(1)
let key = getchar(0)
break
endif
call s:DoWait(1, 0)
endwhile
catch /^Vim:Interrupt$/
endtry
return key
endfunction
function! s:DoTick(mesg, ticker, ...)
let nowT = localtime()
try
execute 'echohl' (a:0 ? a:1 : 'Normal')
echon a:mesg
execute 'echohl' s:hl_cursor
echon a:ticker[nowT % strlen(a:ticker)] "\r"
finally
echohl None
endtry
return nowT
endfunction
" Sleep except for the final round
function! s:DoWait(final, round)
if (a:final - a:round)
sleep 250 m
endif
endfunction
function! s:RequestFile(mesg)
let fname = ''
call s:UntoggleCursor(1)
try
let fname = has('browse') ? browse(0, 'Select file '.a:mesg, './', '')
\ : input('Enter filename '.a:mesg.': ')
catch /^Vim:Interrupt$/
finally
call s:UntoggleCursor(0)
endtry
return fname
endfunction
function! s:SearchWord(comd)
let word = s:DoInput(0, a:comd, '')
if strlen(word)
let @/ = word
call s:DoNormal((a:comd == '/' ? 'n' : 'N'))
endif
endfunction
function! s:ConsumeKey()
let key = getchar(0)
while !s:IsZero(getchar(1))
if !key
call s:UnstickKey(1)
endif
call getchar(0)
endwhile
return key
endfunction
" KLUGE: Without this, special keys hit while in normal mode keep generating
" key codes by themselves, resulting in nearly 100% CPU-time consumption, or
" user-interaction impossibility (vim bug?)
function! s:UnstickKey(force)
if a:force || !s:IsZero(getchar(1))
silent! normal! lh
endif
endfunction
" String manipulation
function! s:StrMatch(str, pat, sub)
" A wrapper function to substitute(). First extract an interesting part
" upon which we perform matching, so that only necessary string (sub) will
" be obtained. An empty string will be returned on failure.
" I took this clever trick from Chalice.
return substitute(matchstr(a:str, a:pat), a:pat, a:sub, '')
endfunction
function! s:StrQuote(str)
let quote = (&shellxquote == '"' ? '\' : '').'"'
return quote.a:str.quote
endfunction
" Remove unnecessary spaces in a string
function! s:StrTrim(str, ...)
let space = (!a:0 || a:1 =~ '^ \=$')
let patrn = space ? '\s' : '\%(\s*\V'.a:1.'\m\s*\)'
let str = substitute(a:str, '[[:cntrl:]]\+', '', 'g') " just in case
let str = substitute(str, '\%(^'.patrn.'\+\|'.patrn.'\+$\)', '', 'g')
return str
endfunction
" Ditto
function! s:StrCompress(str, ...)
let space = (!a:0 || a:1 =~ '^ \=$')
let patrn = space ? '\s' : '\%(\s*\V'.a:1.'\m\s*\)'
let subst = space ? ' ' : a:1
let str = s:StrTrim(a:str, subst)
return substitute(str, patrn.'\+', subst, 'g')
endfunction
" Severer version of the above
function! s:StrSquash(str, ...)
let space = (!a:0 || a:1 =~ '^ \=$')
let patrn = space ? '\s' : '\%(\s*\V'.a:1.'\m\s*\)'
return substitute(a:str, patrn.'\+', '', 'g')
endfunction
function! s:StrMultiply(from, times)
" Super cool logic entirely copied from Chalice
let to = ''
let from = a:from
let times = a:times
while times
if times % 2
" This is binary addition in actuality, performed with strings
let to = to.from
endif
let times = times / 2
let from = from.from
endwhile
return to
endfunction
" Divide string at space or comma
function! s:StrDivide(str, group)
return s:StrMatch(a:str,
\ '^\([^[:space:],]\+\)\=\%([[:space:],]\+\(.*\)\)\=$',
\ '\'.a:group)
endfunction
function! s:EscapeFName(str)
return escape(a:str, '%#')
endfunction
function! s:EscapeMagic(str)
return escape(a:str, '$*.\^~')
endfunction
function! s:EscapeQuote(str)
return escape(a:str, '"\')
endfunction
" Confirmation functions
function! s:Is2KeyCmd(char)
return (a:char =~# "^[zgftFTm`'@Z]$")
endfunction
function! s:IsCtrl_W(key)
" Can accept both key code and char
return (s:Key2Char(a:key) == "\<C-W>")
endfunction
function! s:IsWin3264()
return (has('win32') || has('win32unix') || has('win64'))
endfunction
function! s:IsZero(num)
" Evaluate it as string, since "a:num == 0" unexpectedly (?) evaluates to
" true if a:num is a string value
return (''.a:num == '0')
endfunction
" Validation functions
function! s:ValidatePath(path)
let path = a:path
if strlen(path)
let path = fnamemodify(path, ':p')
let path = substitute(path, '\', '/', 'g')
let path = substitute(path, '/\{2,\}', '/', 'g')
let path = substitute(path, '/$', '', '')
endif
return path
endfunction
function! s:ValidateURL(url)
let url = a:url
if strlen(a:url)
" Cut the unnecessary tail, as in "http://www.foo.com/)."
let url = substitute(a:url, '[),.:;>\]]\+$', '', '')
if !s:IsServerIRC(url)
" "www" stuff
if url !~ '^\a\+://'
let url = 'http://'.url
endif
" domain only, without the final slash
if url =~ '^\a\+://[^/]\+$'
let url = url.'/'
endif
endif
endif
return url
endfunction
" Line functions
function! s:SearchLine(line)
return strlen(a:line) ? search('\m^'.s:EscapeMagic(a:line).'$', 'w') : 0
endfunction
function! s:DelLine() range
call s:ExecuteSilent(a:firstline