" (c) eric johnson " email: vimDebug at iijo dot org " http://iijo.org " -------------------------------------------------------------------- " Check prerequisites. if (!has('perl') || !has('signs')) echo "VimDebug requires +perl and +signs" finish endif " -------------------------------------------------------------------- " Configuration variables. " Make sure all the values remain coherent if you change any. " The VimDebug start key. If this key is not already mapped in " normal mode (nmap), we will map it to start VimDebug. Otherwise, " to start the debugger one can call DBGRstart(...) or use the GUI " with its menu interface. let s:cfg_startKey = "" " GUI menu label. let s:cfg_menuLabel = '&Debugger' " Key bindings and menu settings. Each entry has: key, label, map. let s:cfg_interface = [ \ ['', '&Next', 'DBGRnext()'], \ ['', '&Step in', 'DBGRstep()'], \ ['', 'Step &out', 'DBGRstepout()'], \ ['', '&Continue', 'DBGRcont()'], \ ['b', 'Set &breakpoint', 'DBGRsetBreakPoint()'], \ ['c', 'C&lear breakpoint', 'DBGRclearBreakPoint()'], \ ['ca', 'Clear &all breakpoints', 'DBGRclearAllBreakPoints()'], \ ['x/', '&Print value', 'DBGRprint(inputdialog("Value to print: "))'], \ ['x', 'Print &value here', 'DBGRprint(expand(""))'], \ ['/', 'E&xecute command', 'DBGRcommand(inputdialog("Command to execute: "))'], \ ['', '&Restart', 'DBGRrestart()'], \ ['', '&Quit', 'DBGRquit()'], \] " Global variables. Each entry has: global variable name, default " value. let s:cfg_globals = { \ 'g:DBGRconsoleHeight' : 7, \ 'g:DBGRlineNumbers' : 1, \ 'g:DBGRshowConsole' : 1, \ 'g:DBGRdebugArgs' : "", \} " -------------------------------------------------------------------- " This function will be called at the end of this script to " initialize everything. function! s:Initialize () perl << EOT # Setting up 'lib' like this is useful during development. use Dir::Self; use lib __DIR__ . "/../../.."; # Obtain protocol constant values directly from the Perl # module. This will allow us to use things like "s:k_eor" for # example in our Vim code. use Vim::Debug::Protocol; use Vim::Debug::Daemon; for my $method (qw< k_compilerError k_runtimeError k_dbgrReady k_appExited k_eor k_badCmd k_connect k_disconnect k_doneFile >) { VIM::DoCommand("let s:$method = '" . Vim::Debug::Protocol->$method . "'"); } # Later perl snippets will use these variables. $DBGRsocket1 = 0; $DBGRsocket2 = 0; $EOM = Vim::Debug::Protocol->k_eom . "\r\n"; $EOM_LEN = length $EOM; $PORT = Vim::Debug::Daemon->port; EOT " Colors. hi currentLine term=reverse cterm=reverse gui=reverse hi breakPoint term=NONE cterm=NONE gui=NONE hi empty term=NONE cterm=NONE gui=NONE " Signs. sign define currentLine linehl=currentLine sign define breakPoint linehl=breakPoint text=>> sign define both linehl=currentLine text=>> sign define empty linehl=empty " Initialize globals to their default value, unless they already " have a value. for [l:var, l:dft_val] in items(s:cfg_globals) exec \ "if ! exists('g:" . l:var . "') |" . \ "let " . l:var . " = '" . l:dft_val . "'| " . \ "endif" endfor " Script variables. " The string used to invoke the language's debugger. let s:incantation = "" " 0, the language's debugger is not running; 1, it is running. let s:dbgrIsRunning = 0 " 0, a program is being debugged; 1, no program is being " debugged, or it has done running. let s:programDone = 1 " Could eventually be some other debugger, but currently we " support only Perl. let s:debugger = "Perl" let s:consoleBufNr = -99 let s:bufNr = 0 let s:fileName = "" let s:lineNumber = 0 let s:emptySigns = [] let s:breakPoints = [] let s:sessionId = -1 let s:interfaceSetting = 0 " The user key bindings will be saved here if/when we launch " VimDebug. The entries of this list will be a bit different: " each one will be a two-element list of a key and of a " "saved-map" that will be provided by the 'savemap' vimscript. let s:userSavedkeys = [] " Will be set to 1 (true) if the start key is defined " and we can map to it. let s:canMapStartKey = 0 if s:cfg_startKey != "" && empty(maparg(s:cfg_startKey, "n")) let s:canMapStartKey = 1 endif " Set up the start key and menus. call s:mapStartKey_DBGRstart() call s:VDmenuSet(0) endfunction " -------------------------------------------------------------------- " Debugger functions. " Start the debugger if it's not already running. If there is an " empty string argument, prompt for debugger arguments. function! DBGRstart(...) if s:dbgrIsRunning echo "The debugger is already running." return endif try call s:Incantation(a:000) call s:StartVdd() " do after system() so nongui vim doesn't show a blank screen echo "\rstarting the debugger..." call s:SocketConnect() if has("autocmd") autocmd VimLeave * call DBGRquit() endif call DBGRopenConsole() redraw! call s:HandleCmdResult("connected to VimDebug daemon") call s:Handshake() call s:HandleCmdResult("started the debugger") call s:SocketConnect2() call s:HandleCmdResult2() call _VDsetInterface(1) call s:mapStartKey_toggleKeyBindings() let s:dbgrIsRunning = 1 let s:programDone = 0 catch /AbortLaunch/ echo "Debugger launch aborted." catch /MissingVdd/ echo "vdd is not in your PATH. Something went wrong with your VimDebug install." catch /.*/ echo "Unexpected error: " . v:exception endtry endfunction function! DBGRnext() if !s:Copacetic() return endif echo "\rnext..." call s:SocketWrite("next") call s:HandleCmdResult() endfunction function! DBGRstep() if !s:Copacetic() return endif echo "\rstep..." call s:SocketWrite("step") call s:HandleCmdResult() endfunction function! DBGRstepout() if !s:Copacetic() return endif echo "\rstepout..." call s:SocketWrite("stepout") call s:HandleCmdResult() endfunction function! DBGRcont() if !s:Copacetic() return endif echo "\rcontinue..." call s:SocketWrite("cont") call s:HandleCmdResult() endfunction function! DBGRsetBreakPoint() if !s:Copacetic() return endif let l:currFileName = bufname("%") let l:bufNr = bufnr("%") let l:currLineNr = line(".") let l:id = s:CreateId(l:bufNr, l:currLineNr) if count(s:breakPoints, l:id) == 1 redraw! | echo "\rbreakpoint already set" return endif " tell vdd call s:SocketWrite("break:" . l:currLineNr . ':' . l:currFileName) call add(s:breakPoints, l:id) " check if a currentLine sign is already placed if (s:lineNumber == l:currLineNr) exe "sign unplace " . l:id exe "sign place " . l:id . " line=" . l:currLineNr . " name=both file=" . l:currFileName else exe "sign place " . l:id . " line=" . l:currLineNr . " name=breakPoint file=" . l:currFileName endif call s:HandleCmdResult("breakpoint set") endfunction function! DBGRclearBreakPoint() if !s:Copacetic() return endif let l:currFileName = bufname("%") let l:bufNr = bufnr("%") let l:currLineNr = line(".") let l:id = s:CreateId(l:bufNr, l:currLineNr) if count(s:breakPoints, l:id) == 0 redraw! | echo "\rno breakpoint set here" return endif " tell vdd call s:SocketWrite("clear:" . l:currLineNr . ':' . l:currFileName) call filter(s:breakPoints, 'v:val != l:id') exe "sign unplace " . l:id if(s:lineNumber == l:currLineNr) exe "sign place " . l:id . " line=" . l:currLineNr . " name=currentLine file=" . l:currFileName endif call s:HandleCmdResult("breakpoint disabled") endfunction function! DBGRclearAllBreakPoints() if !s:Copacetic() return endif call s:UnplaceBreakPointSigns() let l:currFileName = bufname("%") let l:bufNr = bufnr("%") let l:currLineNr = line(".") let l:id = s:CreateId(l:bufNr, l:currLineNr) call s:SocketWrite("clearAll") " do this in case the last current line had a break point on it call s:UnplaceTheLastCurrentLineSign() " unplace the old sign call s:PlaceCurrentLineSign(s:lineNumber, s:fileName) " place the new sign call s:HandleCmdResult("all breakpoints disabled") endfunction function! DBGRprint(...) if !s:Copacetic() return endif if a:0 > 0 call s:SocketWrite("print:" . a:1) call s:HandleCmdResult() endif endfunction function! DBGRcommand(...) if !s:Copacetic() return endif echo "" if a:0 > 0 call s:SocketWrite('command:' . a:1) call s:HandleCmdResult() endif endfunction function! DBGRrestart() if ! s:dbgrIsRunning echo "\rthe debugger is not running" return endif call s:SocketWrite("restart") " do after the system() call so that nongui vim doesn't show a blank screen echo "\rrestarting..." call s:UnplaceTheLastCurrentLineSign() redraw! call s:HandleCmdResult("restarted") let s:programDone = 0 endfunction function! DBGRquit() if ! s:dbgrIsRunning echo "\rthe debugger is not running" return endif call _VDsetInterface(0) call s:mapStartKey_DBGRstart() " unplace all signs that were set in this debugging session call s:UnplaceBreakPointSigns() call s:UnplaceEmptySigns() call s:UnplaceTheLastCurrentLineSign() call s:SetNoNumber() call s:SocketWrite("quit") if has("autocmd") autocmd! VimLeave * call DBGRquit() endif " reinitialize script variables let s:lineNumber = 0 let s:fileName = "" let s:bufNr = 0 let s:programDone = 1 let s:dbgrIsRunning = 0 redraw! | echo "\rexited the debugger" " must do this last call DBGRcloseConsole() endfunction " -------------------------------------------------------------------- " Interface handling. " These are the possible values of s:interfaceSetting, which tells us " which key bindings are active and what the GUI menu looks like. " " 0 : User keys, grayed out menu entries. " 1 : VimDebug keys, active menu entries. " 2 : User keys, active menu entries, keys in parentheses. " Request interface setting 0, 1, or 2, or 3 to toggle between 1 " and 2. function! _VDsetInterface(request) if a:request == 3 if s:interfaceSetting == 0 return endif " Toggle between 1 and 2. let l:want = 3 - s:interfaceSetting else let l:want = a:request endif if l:want == 0 || l:want == 2 call s:VDrestoreKeyBindings() elseif l:want == 1 call s:VDsetKeyBindings() else return endif call s:VDmenuSet(l:want) let s:interfaceSetting = l:want endfunction function! s:VDsetKeyBindings () let s:userSavedkeys = [] for l:data in s:cfg_interface let l:key = l:data[0] let l:map = l:data[2] call add(s:userSavedkeys, [l:key, savemap#save_map("n", l:key)]) exec "nmap " . l:key . " :call " . l:map . "" endfor echo "VimDebug keys are active." endfunction function! s:VDrestoreKeyBindings () for l:key_savedmap in s:userSavedkeys let l:key = l:key_savedmap[0] let l:saved_map = l:key_savedmap[1] if empty(l:saved_map['__map_info'][0]['normal']) exec "unmap " . l:key else call l:saved_map.restore() endif endfor let s:userSavedkeys = [] echo "User keys are active." endfunction function! s:VDmenu_Start (on_or_off) if a:on_or_off == 1 exec "amenu " . s:cfg_menuLabel . ".Start :call DBGRstart(\"\")" else exec "amenu disable " . s:cfg_menuLabel . ".Start" endif endfunction function! s:VDmenu_Toggle (on_or_off) if a:on_or_off == 1 exec "amenu " . s:cfg_menuLabel . ".To&ggle\\ key\\ bindings :call _VDsetInterface(3)" else exec "amenu disable " . s:cfg_menuLabel . ".To&ggle\\ key\\ bindings" endif endfunction " Set up the GUI menu. function! s:VDmenuSet (request) if ! has("gui_running") return endif " Delete the existing menu. try exec ":aunmenu " . s:cfg_menuLabel catch endtry " Insert the first three menu lines. call s:VDmenu_Start(1) call s:VDmenu_Toggle(1) exec "amenu ". s:cfg_menuLabel . ".-separ- :" " Disable the relevant one. if a:request == 0 call s:VDmenu_Toggle(0) else call s:VDmenu_Start(0) endif " Build the other menu entries. for l:data in s:cfg_interface let l:key = l:data[0] let l:label = l:data[1] let l:map = l:data[2] let l:esc_label_key = escape(l:label . "\t" . l:key, " \t") try if a:request == 0 exec "amenu disable " . s:cfg_menuLabel . "." . l:esc_label_key elseif a:request == 1 exec "amenu " . s:cfg_menuLabel . "." . l:esc_label_key . " :call " . l:map . "" else let l:esc_label_no_key = escape(l:label . "\t(" . l:key . ")", " \t") exec "amenu " . s:cfg_menuLabel . "." . l:esc_label_no_key . " :call " . l:map . "" endif catch endtry endfor endfunction function! s:mapStartKey_DBGRstart () if s:canMapStartKey exec "nmap " . s:cfg_startKey . " :call DBGRstart(\"\")" endif endfunction function! s:mapStartKey_toggleKeyBindings () if s:canMapStartKey exec "nmap " . s:cfg_startKey . " :call _VDsetInterface(3)" endif endfunction " -------------------------------------------------------------------- " User commands. command! -nargs=* VDstart call DBGRstart() command! -nargs=0 VDtoggleKeys call _VDsetInterface(3) " -------------------------------------------------------------------- " Utility functions. " Returns 1 if everything is copacetic, 0 otherwise. function! s:Copacetic() if s:dbgrIsRunning != 1 echo "\rthe debugger is not running" return 0 elseif s:programDone echo "\rthe application being debugged terminated" return 0 endif return 1 endfunction function! s:PlaceEmptySign() let l:id = s:CreateId(bufnr("%"), "1") if count(s:emptySigns, l:id) == 0 let l:fileName = bufname("%") call add(s:emptySigns, l:id) exe "sign place " . l:id . " line=1 name=empty file=" . l:fileName endif endfunction function! s:UnplaceEmptySigns() let l:oldBufNr = bufnr("%") for l:id in s:emptySigns let l:bufNr = s:BufNrFromId(l:id) if bufexists(l:bufNr) != 0 if bufnr("%") != l:bufNr exe "buffer " . l:bufNr endif exe "sign unplace " . l:id exe "buffer " . l:oldBufNr endif endfor let s:emptySigns = [] endfunction function! s:UnplaceBreakPointSigns() let l:oldBufNr = bufnr("%") for l:id in s:breakPoints let l:bufNr = s:BufNrFromId(l:id) if bufexists(l:bufNr) != 0 if bufnr("%") != l:bufNr exe "buffer " . l:bufNr endif exe "sign unplace " . l:id exe "buffer " . l:oldBufNr endif endfor let s:breakPoints = [] endfunction function! s:SetNumber() if g:DBGRlineNumbers == 1 set number endif endfunction function! s:SetNoNumber() if g:DBGRlineNumbers == 1 set nonumber endif endfunction function! s:CreateId(bufNr, lineNumber) return a:bufNr * 10000000 + a:lineNumber endfunction function! s:BufNrFromId(id) return a:id / 10000000 endfunction function! s:LineNrFromId(id) return a:id % 10000000 endfunction function! s:Incantation(dbgr_args_list) try let s:bufNr = bufnr("%") let s:fileName = bufname("%") if s:fileName == "" throw "NoFileToDebug" endif let l:nb_dbgr_args = len(a:dbgr_args_list) let g:DBGRdebugArgs = \ l:nb_dbgr_args == 0 \ ? "" \ : l:nb_dbgr_args == 1 && a:dbgr_args_list[0] == "" \ ? inputdialog("Enter arguments for debugging, if any: ", g:DBGRdebugArgs) \ : join(a:dbgr_args_list) let s:incantation = "perl -Ilib -d " . s:fileName if g:DBGRdebugArgs != "" let s:incantation .= " " . g:DBGRdebugArgs endif catch /NoFileToDebug/ echo "No file to debug." throw "AbortLaunch" catch echo "Exception caught: " . v:exception throw "AbortLaunch" endtry endfunction function! s:HandleCmdResult(...) let l:cmdResult = split(s:SocketRead(), s:k_eor, 1) let [l:status, l:lineNumber, l:fileName, l:value, l:output] = l:cmdResult if l:status == s:k_dbgrReady call s:ConsolePrint(l:output) if len(l:lineNumber) > 0 call s:CurrentLineMagic(l:lineNumber, l:fileName) endif elseif l:status == s:k_appExited call s:ConsolePrint(l:output) call s:HandleProgramTermination() redraw! | echo "The application being debugged terminated." elseif l:status == s:k_compilerError call s:ConsolePrint(l:output) call s:HandleProgramTermination() redraw! | echo "The program did not compile." elseif l:status == s:k_runtimeError call s:ConsolePrint(l:output) call s:HandleProgramTermination() redraw! | echo "There was a runtime error." elseif l:status == s:k_connect let s:sessionId = l:value elseif l:status == s:k_disconnect echo "disconnected" else echo " error:001. Something bad happened. Please report this to vimdebug at iijo dot org" echo got endif return endfunction function! s:HandleCmdResult2(...) let l:foo = s:SocketRead2() endfunction " - jumps to the lineNumber in the file, fileName " - highlights the current line " - returns nothing function! s:CurrentLineMagic(lineNumber, fileName) let l:lineNumber = a:lineNumber let l:fileName = a:fileName let l:fileName = s:JumpToLine(l:lineNumber, l:fileName) " if no signs placed in this file, place an invisible one on line 1. " otherwise, the code will shift left when the old currentline sign is " unplaced and then shift right again when the new currentline sign is " placed. and thats really annoying for the user. call s:PlaceEmptySign() call s:UnplaceTheLastCurrentLineSign() " unplace the old sign call s:PlaceCurrentLineSign(l:lineNumber, l:fileName) " place the new sign call s:SetNumber() "z. " scroll page so that this line is in the middle " set script variables for next time let s:lineNumber = l:lineNumber let s:fileName = l:fileName return endfunction " the fileName may have been changed if we stepped into a library or some " other piece of code in an another file. load the new file if thats " necessary and then jump to lineNumber " " returns a fileName. function! s:JumpToLine(lineNumber, fileName) let l:fileName = a:fileName " no buffer with this file has been loaded if !bufexists(bufname(l:fileName)) exe ":e! " . l:fileName endif let l:winNr = bufwinnr(bufnr(l:fileName)) if l:winNr != -1 exe l:winNr . "wincmd w" endif " make a:fileName the current buffer if bufname(l:fileName) != bufname("%") exe ":buffer " . bufnr(l:fileName) endif " jump to line exe ":" . a:lineNumber normal z. if foldlevel(a:lineNumber) != 0 normal zo endif return bufname(l:fileName) endfunction function! s:UnplaceTheLastCurrentLineSign() let l:lastId = s:CreateId(s:bufNr, s:lineNumber) exe 'sign unplace ' . l:lastId if count(s:breakPoints, l:lastId) == 1 exe "sign place " . l:lastId . " line=" . s:lineNumber . " name=breakPoint file=" . s:fileName endif endfunction function! s:PlaceCurrentLineSign(lineNumber, fileName) let l:bufNr = bufnr(a:fileName) let s:bufNr = l:bufNr let l:id = s:CreateId(l:bufNr, a:lineNumber) if count(s:breakPoints, l:id) == 1 exe "sign place " . l:id . \ " line=" . a:lineNumber . " name=both file=" . a:fileName else exe "sign place " . l:id . \ " line=" . a:lineNumber . " name=currentLine file=" . a:fileName endif endfunction function! s:HandleProgramTermination() call s:UnplaceTheLastCurrentLineSign() let s:lineNumber = 0 let s:bufNr = 0 let s:programDone = 1 endfunction " -------------------------------------------------------------------- " Debugger console functions. function! DBGRopenConsole() if g:DBGRshowConsole == 0 return 0 endif new "debugger console" let s:consoleBufNr = bufnr('%') exe "resize " . g:DBGRconsoleHeight exe "sign place 9999 line=1 name=empty buffer=" . s:consoleBufNr call s:SetNumber() set buftype=nofile wincmd p endfunction function! DBGRcloseConsole() if g:DBGRshowConsole == 0 return 0 endif let l:consoleWinNr = bufwinnr(s:consoleBufNr) if l:consoleWinNr == -1 return endif exe l:consoleWinNr . "wincmd w" q endfunction function! s:ConsolePrint(msg) if g:DBGRshowConsole == 0 return 0 endif let l:consoleWinNr = bufwinnr(s:consoleBufNr) if l:consoleWinNr == -1 "call confirm(a:msg, "&Ok") call DBGRopenConsole() let l:consoleWinNr = bufwinnr(s:consoleBufNr) endif silent exe l:consoleWinNr . "wincmd w" let l:oldValue = @x let @x = a:msg silent exe 'normal G$"xp' let @x = l:oldValue normal G wincmd p endfunction " -------------------------------------------------------------------- " Socket functions. function! s:StartVdd() if !executable('vdd') throw "MissingVdd" endif exec "silent :! vdd &" endfunction function! s:Handshake() let l:msg = "start:" . s:sessionId . \ ":" . s:debugger . \ ":" . s:incantation call s:SocketWrite(l:msg) endfunction function! s:SocketConnect() perl << EOF use IO::Socket; foreach my $i (0..9) { $DBGRsocket1 = IO::Socket::INET->new( Proto => "tcp", PeerAddr => "localhost", PeerPort => $PORT, ); return if defined $DBGRsocket1; sleep 1; } my $msg = "cannot connect to port $PORT at localhost"; VIM::Msg($msg); VIM::DoCommand("throw '${msg}'"); EOF endfunction function! s:SocketConnect2() perl << EOF use IO::Socket; foreach my $i (0..9) { $DBGRsocket2 = IO::Socket::INET->new( Proto => "tcp", PeerAddr => "localhost", PeerPort => $PORT, ); return if defined $DBGRsocket2; sleep 1; } my $msg = "cannot connect to port $PORT at localhost"; VIM::Msg($msg); VIM::DoCommand("throw '${msg}'"); EOF endfunction function! s:SocketRead() try " yeah this is a very inefficient but non blocking loop. " vdd signals that its done sending a msg when it touches the file. " while VimDebug thinks, the user can cancel their operation. while !filereadable(s:k_doneFile) endwhile catch /Vim:Interrupt/ echom "action cancelled" call s:SocketWrite2('stop:' . s:sessionId) " disconnect call s:HandleCmdResult2() " handle disconnect call s:SocketConnect2() " reconnect call s:HandleCmdResult2() " handle reconnect endtry perl << EOF my $data = ''; $data .= <$DBGRsocket1> until substr($data, -1 * $EOM_LEN) eq $EOM; $data .= <$DBGRsocket1> until substr($data, -1 * $EOM_LEN) eq $EOM; $data = substr($data, 0, -1 * $EOM_LEN); # chop EOM $data =~ s|'|''|g; # escape single quotes ' VIM::DoCommand("call delete(s:k_doneFile)"); VIM::DoCommand("return '" . $data . "'"); EOF endfunction function! s:SocketRead2() try " yeah this is a very inefficient but non blocking loop. " vdd signals that its done sending a msg when it touches the file. " while VimDebug thinks, the user can cancel their operation. while !filereadable(s:k_doneFile) endwhile endtry perl << EOF my $data = ''; $data .= <$DBGRsocket2> until substr($data, -1 * $EOM_LEN) eq $EOM; $data .= <$DBGRsocket2> until substr($data, -1 * $EOM_LEN) eq $EOM; $data = substr($data, 0, -1 * $EOM_LEN); # chop EOM $data =~ s|'|''|g; # escape single quotes ' VIM::DoCommand("call delete(s:k_doneFile)"); VIM::DoCommand("return '" . $data . "'"); EOF endfunction function! s:SocketWrite(data) perl print $DBGRsocket1 VIM::Eval('a:data') . "\n"; endfunction function! s:SocketWrite2(data) perl print $DBGRsocket2 VIM::Eval('a:data') . "\n"; endfunction " -------------------------------------------------------------------- " Initialize everything. call s:Initialize()