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 let s:ptywin = win_getid(winnr()) 58 59 " Create a hidden terminal window to communicate with gdb 60 let s:commbuf = term_start('NONE', { 61 \ 'term_name': 'gdb communication', 62 \ 'out_cb': function('s:CommOutput'), 63 \ 'hidden': 1, 64 \ }) 65 if s:commbuf == 0 66 echoerr 'Failed to open the communication terminal window' 67 exe 'bwipe! ' . s:ptybuf 68 return 69 endif 70 let commpty = job_info(term_getjob(s:commbuf))['tty_out'] 71 72 " Open a terminal window to run the debugger. 73 let cmd = [g:termdebugger, '-tty', pty, a:cmd] 74 echomsg 'executing "' . join(cmd) . '"' 75 let gdbbuf = term_start(cmd, { 76 \ 'exit_cb': function('s:EndDebug'), 77 \ 'term_finish': 'close', 78 \ }) 79 if gdbbuf == 0 80 echoerr 'Failed to open the gdb terminal window' 81 exe 'bwipe! ' . s:ptybuf 82 exe 'bwipe! ' . s:commbuf 83 return 84 endif 85 let s:gdbwin = win_getid(winnr()) 86 87 " Connect gdb to the communication pty, using the GDB/MI interface 88 call term_sendkeys(gdbbuf, 'new-ui mi ' . commpty . "\r") 89 90 " Install debugger commands in the text window. 91 call win_gotoid(s:startwin) 92 call s:InstallCommands() 93 call win_gotoid(s:gdbwin) 94 95 let s:breakpoints = {} 96endfunc 97 98func s:EndDebug(job, status) 99 exe 'bwipe! ' . s:ptybuf 100 exe 'bwipe! ' . s:commbuf 101 102 let curwinid = win_getid(winnr()) 103 104 call win_gotoid(s:startwin) 105 let &signcolumn = s:startsigncolumn 106 call s:DeleteCommands() 107 108 call win_gotoid(curwinid) 109endfunc 110 111" Handle a message received from gdb on the GDB/MI interface. 112func s:CommOutput(chan, msg) 113 let msgs = split(a:msg, "\r") 114 115 for msg in msgs 116 " remove prefixed NL 117 if msg[0] == "\n" 118 let msg = msg[1:] 119 endif 120 if msg != '' 121 if msg =~ '^\*\(stopped\|running\)' 122 call s:HandleCursor(msg) 123 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 124 call s:HandleNewBreakpoint(msg) 125 elseif msg =~ '^=breakpoint-deleted,' 126 call s:HandleBreakpointDelete(msg) 127 elseif msg =~ '^\^done,value=' 128 call s:HandleEvaluate(msg) 129 elseif msg =~ '^\^error,msg=' 130 call s:HandleError(msg) 131 endif 132 endif 133 endfor 134endfunc 135 136" Install commands in the current window to control the debugger. 137func s:InstallCommands() 138 command Break call s:SetBreakpoint() 139 command Delete call s:DeleteBreakpoint() 140 command Step call s:SendCommand('-exec-step') 141 command Over call s:SendCommand('-exec-next') 142 command Finish call s:SendCommand('-exec-finish') 143 command Continue call s:SendCommand('-exec-continue') 144 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 145 command Gdb call win_gotoid(s:gdbwin) 146 command Program call win_gotoid(s:ptywin) 147 148 " TODO: can the K mapping be restored? 149 nnoremap K :Evaluate<CR> 150endfunc 151 152" Delete installed debugger commands in the current window. 153func s:DeleteCommands() 154 delcommand Break 155 delcommand Delete 156 delcommand Step 157 delcommand Over 158 delcommand Finish 159 delcommand Continue 160 delcommand Evaluate 161 delcommand Gdb 162 delcommand Program 163 164 nunmap K 165 sign undefine debugPC 166 sign undefine debugBreakpoint 167 exe 'sign unplace ' . s:pc_id 168 for key in keys(s:breakpoints) 169 exe 'sign unplace ' . (s:break_id + key) 170 endfor 171 unlet s:breakpoints 172endfunc 173 174" :Break - Set a breakpoint at the cursor position. 175func s:SetBreakpoint() 176 call term_sendkeys(s:commbuf, '-break-insert --source ' 177 \ . fnameescape(expand('%:p')) . ' --line ' . line('.') . "\r") 178endfunc 179 180" :Delete - Delete a breakpoint at the cursor position. 181func s:DeleteBreakpoint() 182 let fname = fnameescape(expand('%:p')) 183 let lnum = line('.') 184 for [key, val] in items(s:breakpoints) 185 if val['fname'] == fname && val['lnum'] == lnum 186 call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r") 187 " Assume this always wors, the reply is simply "^done". 188 exe 'sign unplace ' . (s:break_id + key) 189 unlet s:breakpoints[key] 190 break 191 endif 192 endfor 193endfunc 194 195" :Next, :Continue, etc - send a command to gdb 196func s:SendCommand(cmd) 197 call term_sendkeys(s:commbuf, a:cmd . "\r") 198endfunc 199 200" :Evaluate - evaluate what is under the cursor 201func s:Evaluate(range, arg) 202 if a:arg != '' 203 let expr = a:arg 204 elseif a:range == 2 205 let pos = getcurpos() 206 let reg = getreg('v', 1, 1) 207 let regt = getregtype('v') 208 normal! gv"vy 209 let expr = @v 210 call setpos('.', pos) 211 call setreg('v', reg, regt) 212 else 213 let expr = expand('<cexpr>') 214 endif 215 call term_sendkeys(s:commbuf, '-data-evaluate-expression "' . expr . "\"\r") 216 let s:evalexpr = expr 217endfunc 218 219" Handle the result of data-evaluate-expression 220func s:HandleEvaluate(msg) 221 echomsg '"' . s:evalexpr . '": ' . substitute(a:msg, '.*value="\(.*\)"', '\1', '') 222endfunc 223 224" Handle an error. 225func s:HandleError(msg) 226 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 227endfunc 228 229" Handle stopping and running message from gdb. 230" Will update the sign that shows the current position. 231func s:HandleCursor(msg) 232 let wid = win_getid(winnr()) 233 234 if win_gotoid(s:startwin) 235 if a:msg =~ '^\*stopped' 236 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 237 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 238 if lnum =~ '^[0-9]*$' 239 if expand('%:h') != fname 240 if &modified 241 " TODO: find existing window 242 exe 'split ' . fnameescape(fname) 243 let s:startwin = win_getid(winnr()) 244 else 245 exe 'edit ' . fnameescape(fname) 246 endif 247 endif 248 exe lnum 249 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fnameescape(fname) 250 setlocal signcolumn=yes 251 endif 252 else 253 exe 'sign unplace ' . s:pc_id 254 endif 255 256 call win_gotoid(wid) 257 endif 258endfunc 259 260" Handle setting a breakpoint 261" Will update the sign that shows the breakpoint 262func s:HandleNewBreakpoint(msg) 263 let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0 264 if nr == 0 265 return 266 endif 267 268 if has_key(s:breakpoints, nr) 269 let entry = s:breakpoints[nr] 270 else 271 let entry = {} 272 let s:breakpoints[nr] = entry 273 endif 274 275 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 276 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 277 278 exe 'sign place ' . (s:break_id + nr) . ' line=' . lnum . ' name=debugBreakpoint file=' . fnameescape(fname) 279 280 let entry['fname'] = fname 281 let entry['lnum'] = lnum 282endfunc 283 284" Handle deleting a breakpoint 285" Will remove the sign that shows the breakpoint 286func s:HandleBreakpointDelete(msg) 287 let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 288 if nr == 0 289 return 290 endif 291 exe 'sign unplace ' . (s:break_id + nr) 292 unlet s:breakpoints[nr] 293endfunc 294