1" Debugger plugin using gdb. 2" 3" WORK IN PROGRESS - much doesn't work yet 4" 5" Open two visible terminal windows: 6" 1. run a pty, as with ":term NONE" 7" 2. run gdb, passing the pty 8" The current window is used to view source code and follows gdb. 9" 10" A third terminal window is hidden, it is used for communication with gdb. 11" 12" The communication with gdb uses GDB/MI. See: 13" https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html 14" 15" Author: Bram Moolenaar 16" Copyright: Vim license applies, see ":help license" 17 18" The command that starts debugging, e.g. ":Termdebug vim". 19" To end type "quit" in the gdb window. 20command -nargs=* -complete=file Termdebug call s:StartDebug(<q-args>) 21 22" Name of the gdb command, defaults to "gdb". 23if !exists('termdebugger') 24 let termdebugger = 'gdb' 25endif 26 27" Sign used to highlight the line where the program has stopped. 28" There can be only one. 29sign define debugPC linehl=debugPC 30let s:pc_id = 12 31let s:break_id = 13 32 33" Sign used to indicate a breakpoint. 34" Can be used multiple times. 35sign define debugBreakpoint text=>> texthl=debugBreakpoint 36 37if &background == 'light' 38 hi default debugPC term=reverse ctermbg=lightblue guibg=lightblue 39else 40 hi default debugPC term=reverse ctermbg=darkblue guibg=darkblue 41endif 42hi default debugBreakpoint term=reverse ctermbg=red guibg=red 43 44func s:StartDebug(cmd) 45 let s:startwin = win_getid(winnr()) 46 let s:startsigncolumn = &signcolumn 47 48 " Open a terminal window without a job, to run the debugged program 49 let s:ptybuf = term_start('NONE', { 50 \ 'term_name': 'gdb program', 51 \ }) 52 if s:ptybuf == 0 53 echoerr 'Failed to open the program terminal window' 54 return 55 endif 56 let pty = job_info(term_getjob(s:ptybuf))['tty_out'] 57 58 " Create a hidden terminal window to communicate with gdb 59 let s:commbuf = term_start('NONE', { 60 \ 'term_name': 'gdb communication', 61 \ 'out_cb': function('s:CommOutput'), 62 \ 'hidden': 1, 63 \ }) 64 if s:commbuf == 0 65 echoerr 'Failed to open the communication terminal window' 66 exe 'bwipe! ' . s:ptybuf 67 return 68 endif 69 let commpty = job_info(term_getjob(s:commbuf))['tty_out'] 70 71 " Open a terminal window to run the debugger. 72 let cmd = [g:termdebugger, '-tty', pty, a:cmd] 73 echomsg 'executing "' . join(cmd) . '"' 74 let gdbbuf = term_start(cmd, { 75 \ 'exit_cb': function('s:EndDebug'), 76 \ 'term_finish': 'close', 77 \ }) 78 if gdbbuf == 0 79 echoerr 'Failed to open the gdb terminal window' 80 exe 'bwipe! ' . s:ptybuf 81 exe 'bwipe! ' . s:commbuf 82 return 83 endif 84 85 " Connect gdb to the communication pty, using the GDB/MI interface 86 call term_sendkeys(gdbbuf, 'new-ui mi ' . commpty . "\r") 87 88 " Install debugger commands. 89 call s:InstallCommands() 90 91 let s:breakpoints = {} 92endfunc 93 94func s:EndDebug(job, status) 95 exe 'bwipe! ' . s:ptybuf 96 exe 'bwipe! ' . s:commbuf 97 98 let curwinid = win_getid(winnr()) 99 100 call win_gotoid(s:startwin) 101 let &signcolumn = s:startsigncolumn 102 call s:DeleteCommands() 103 104 call win_gotoid(curwinid) 105endfunc 106 107" Handle a message received from gdb on the GDB/MI interface. 108func s:CommOutput(chan, msg) 109 let msgs = split(a:msg, "\r") 110 111 for msg in msgs 112 " remove prefixed NL 113 if msg[0] == "\n" 114 let msg = msg[1:] 115 endif 116 if msg != '' 117 if msg =~ '^\*\(stopped\|running\)' 118 call s:HandleCursor(msg) 119 elseif msg =~ '^\^done,bkpt=' 120 call s:HandleNewBreakpoint(msg) 121 elseif msg =~ '^=breakpoint-deleted,' 122 call s:HandleBreakpointDelete(msg) 123 endif 124 endif 125 endfor 126endfunc 127 128" Install commands in the current window to control the debugger. 129func s:InstallCommands() 130 command Break call s:SetBreakpoint() 131 command Delete call s:DeleteBreakpoint() 132 command Step call s:SendCommand('-exec-step') 133 command NNext call s:SendCommand('-exec-next') 134 command Finish call s:SendCommand('-exec-finish') 135 command Continue call s:SendCommand('-exec-continue') 136endfunc 137 138" Delete installed debugger commands in the current window. 139func s:DeleteCommands() 140 delcommand Break 141 delcommand Delete 142 delcommand Step 143 delcommand NNext 144 delcommand Finish 145 delcommand Continue 146endfunc 147 148" :Break - Set a breakpoint at the cursor position. 149func s:SetBreakpoint() 150 call term_sendkeys(s:commbuf, '-break-insert --source ' 151 \ . fnameescape(expand('%:p')) . ' --line ' . line('.') . "\r") 152endfunc 153 154" :Delete - Delete a breakpoint at the cursor position. 155func s:DeleteBreakpoint() 156 let fname = fnameescape(expand('%:p')) 157 let lnum = line('.') 158 for [key, val] in items(s:breakpoints) 159 if val['fname'] == fname && val['lnum'] == lnum 160 call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r") 161 " Assume this always wors, the reply is simply "^done". 162 exe 'sign unplace ' . (s:break_id + key) 163 unlet s:breakpoints[key] 164 break 165 endif 166 endfor 167endfunc 168 169" :Next, :Continue, etc - send a command to gdb 170func s:SendCommand(cmd) 171 call term_sendkeys(s:commbuf, a:cmd . "\r") 172endfunc 173 174" Handle stopping and running message from gdb. 175" Will update the sign that shows the current position. 176func s:HandleCursor(msg) 177 let wid = win_getid(winnr()) 178 179 if win_gotoid(s:startwin) 180 if a:msg =~ '^\*stopped' 181 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 182 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 183 if lnum =~ '^[0-9]*$' 184 if expand('%:h') != fname 185 if &modified 186 " TODO: find existing window 187 exe 'split ' . fnameescape(fname) 188 let s:startwin = win_getid(winnr()) 189 else 190 exe 'edit ' . fnameescape(fname) 191 endif 192 endif 193 exe lnum 194 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fnameescape(fname) 195 setlocal signcolumn=yes 196 endif 197 else 198 exe 'sign unplace ' . s:pc_id 199 endif 200 201 call win_gotoid(wid) 202 endif 203endfunc 204 205" Handle setting a breakpoint 206" Will update the sign that shows the breakpoint 207func s:HandleNewBreakpoint(msg) 208 let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0 209 if nr == 0 210 return 211 endif 212 213 if has_key(s:breakpoints, nr) 214 let entry = s:breakpoints[nr] 215 else 216 let entry = {} 217 let s:breakpoints[nr] = entry 218 endif 219 220 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 221 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 222 223 exe 'sign place ' . (s:break_id + nr) . ' line=' . lnum . ' name=debugBreakpoint file=' . fnameescape(fname) 224 225 let entry['fname'] = fname 226 let entry['lnum'] = lnum 227endfunc 228 229" Handle deleting a breakpoint 230" Will remove the sign that shows the breakpoint 231func s:HandleBreakpointDelete(msg) 232 let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 233 if nr == 0 234 return 235 endif 236 exe 'sign unplace ' . (s:break_id + nr) 237 unlet s:breakpoints[nr] 238endfunc 239