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