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