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" Uncomment this line to write logging in "debuglog". 24" call ch_logfile('debuglog', 'w') 25 26" The command that starts debugging, e.g. ":Termdebug vim". 27" To end type "quit" in the gdb window. 28command -nargs=* -complete=file Termdebug call s:StartDebug(<q-args>) 29 30" Name of the gdb command, defaults to "gdb". 31if !exists('termdebugger') 32 let termdebugger = 'gdb' 33endif 34 35let s:pc_id = 12 36let s:break_id = 13 37let s:stopped = 1 38 39if &background == 'light' 40 hi default debugPC term=reverse ctermbg=lightblue guibg=lightblue 41else 42 hi default debugPC term=reverse ctermbg=darkblue guibg=darkblue 43endif 44hi default debugBreakpoint term=reverse ctermbg=red guibg=red 45 46func s:StartDebug(cmd) 47 let s:startwin = win_getid(winnr()) 48 let s:startsigncolumn = &signcolumn 49 50 let s:save_columns = 0 51 if exists('g:termdebug_wide') 52 if &columns < g:termdebug_wide 53 let s:save_columns = &columns 54 let &columns = g:termdebug_wide 55 endif 56 let vertical = 1 57 else 58 let vertical = 0 59 endif 60 61 " Open a terminal window without a job, to run the debugged program 62 let s:ptybuf = term_start('NONE', { 63 \ 'term_name': 'gdb program', 64 \ 'vertical': vertical, 65 \ }) 66 if s:ptybuf == 0 67 echoerr 'Failed to open the program terminal window' 68 return 69 endif 70 let pty = job_info(term_getjob(s:ptybuf))['tty_out'] 71 let s:ptywin = win_getid(winnr()) 72 if vertical 73 " Assuming the source code window will get a signcolumn, use two more 74 " columns for that, thus one less for the terminal window. 75 exe (&columns / 2 - 1) . "wincmd |" 76 endif 77 78 " Create a hidden terminal window to communicate with gdb 79 let s:commbuf = term_start('NONE', { 80 \ 'term_name': 'gdb communication', 81 \ 'out_cb': function('s:CommOutput'), 82 \ 'hidden': 1, 83 \ }) 84 if s:commbuf == 0 85 echoerr 'Failed to open the communication terminal window' 86 exe 'bwipe! ' . s:ptybuf 87 return 88 endif 89 let commpty = job_info(term_getjob(s:commbuf))['tty_out'] 90 91 " Open a terminal window to run the debugger. 92 " Add -quiet to avoid the intro message causing a hit-enter prompt. 93 let cmd = [g:termdebugger, '-quiet', '-tty', pty, a:cmd] 94 echomsg 'executing "' . join(cmd) . '"' 95 let s:gdbbuf = term_start(cmd, { 96 \ 'exit_cb': function('s:EndDebug'), 97 \ 'term_finish': 'close', 98 \ }) 99 if s:gdbbuf == 0 100 echoerr 'Failed to open the gdb terminal window' 101 exe 'bwipe! ' . s:ptybuf 102 exe 'bwipe! ' . s:commbuf 103 return 104 endif 105 let s:gdbwin = win_getid(winnr()) 106 107 " Connect gdb to the communication pty, using the GDB/MI interface 108 " If you get an error "undefined command" your GDB is too old. 109 call term_sendkeys(s:gdbbuf, 'new-ui mi ' . commpty . "\r") 110 111 " Interpret commands while the target is running. This should usualy only be 112 " exec-interrupt, since many commands don't work properly while the target is 113 " running. 114 call s:SendCommand('-gdb-set mi-async on') 115 116 " Sign used to highlight the line where the program has stopped. 117 " There can be only one. 118 sign define debugPC linehl=debugPC 119 120 " Sign used to indicate a breakpoint. 121 " Can be used multiple times. 122 sign define debugBreakpoint text=>> texthl=debugBreakpoint 123 124 " Install debugger commands in the text window. 125 call win_gotoid(s:startwin) 126 call s:InstallCommands() 127 call win_gotoid(s:gdbwin) 128 129 " Enable showing a balloon with eval info 130 if has("balloon_eval") 131 set ballooneval 132 set balloonexpr=TermDebugBalloonExpr() 133 if has("balloon_eval_term") 134 set balloonevalterm 135 endif 136 endif 137 138 let s:breakpoints = {} 139 140 augroup TermDebug 141 au BufRead * call s:BufRead() 142 au BufUnload * call s:BufUnloaded() 143 augroup END 144endfunc 145 146func s:EndDebug(job, status) 147 exe 'bwipe! ' . s:ptybuf 148 exe 'bwipe! ' . s:commbuf 149 150 let curwinid = win_getid(winnr()) 151 152 call win_gotoid(s:startwin) 153 let &signcolumn = s:startsigncolumn 154 call s:DeleteCommands() 155 156 call win_gotoid(curwinid) 157 if s:save_columns > 0 158 let &columns = s:save_columns 159 endif 160 161 if has("balloon_eval") 162 set noballooneval 163 set balloonexpr= 164 if has("balloon_eval_term") 165 set noballoonevalterm 166 endif 167 endif 168 169 au! TermDebug 170endfunc 171 172" Handle a message received from gdb on the GDB/MI interface. 173func s:CommOutput(chan, msg) 174 let msgs = split(a:msg, "\r") 175 176 for msg in msgs 177 " remove prefixed NL 178 if msg[0] == "\n" 179 let msg = msg[1:] 180 endif 181 if msg != '' 182 if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)' 183 call s:HandleCursor(msg) 184 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 185 call s:HandleNewBreakpoint(msg) 186 elseif msg =~ '^=breakpoint-deleted,' 187 call s:HandleBreakpointDelete(msg) 188 elseif msg =~ '^\^done,value=' 189 call s:HandleEvaluate(msg) 190 elseif msg =~ '^\^error,msg=' 191 call s:HandleError(msg) 192 endif 193 endif 194 endfor 195endfunc 196 197" Install commands in the current window to control the debugger. 198func s:InstallCommands() 199 command Break call s:SetBreakpoint() 200 command Delete call s:DeleteBreakpoint() 201 command Step call s:SendCommand('-exec-step') 202 command Over call s:SendCommand('-exec-next') 203 command Finish call s:SendCommand('-exec-finish') 204 command -nargs=* Run call s:Run(<q-args>) 205 command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>) 206 command Stop call s:SendCommand('-exec-interrupt') 207 command Continue call s:SendCommand('-exec-continue') 208 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 209 command Gdb call win_gotoid(s:gdbwin) 210 command Program call win_gotoid(s:ptywin) 211 212 " TODO: can the K mapping be restored? 213 nnoremap K :Evaluate<CR> 214 215 if has('menu') 216 nnoremenu WinBar.Step :Step<CR> 217 nnoremenu WinBar.Next :Over<CR> 218 nnoremenu WinBar.Finish :Finish<CR> 219 nnoremenu WinBar.Cont :Continue<CR> 220 nnoremenu WinBar.Stop :Stop<CR> 221 nnoremenu WinBar.Eval :Evaluate<CR> 222 endif 223endfunc 224 225" Delete installed debugger commands in the current window. 226func s:DeleteCommands() 227 delcommand Break 228 delcommand Delete 229 delcommand Step 230 delcommand Over 231 delcommand Finish 232 delcommand Run 233 delcommand Arguments 234 delcommand Stop 235 delcommand Continue 236 delcommand Evaluate 237 delcommand Gdb 238 delcommand Program 239 240 nunmap K 241 242 if has('menu') 243 aunmenu WinBar.Step 244 aunmenu WinBar.Next 245 aunmenu WinBar.Finish 246 aunmenu WinBar.Cont 247 aunmenu WinBar.Stop 248 aunmenu WinBar.Eval 249 endif 250 251 exe 'sign unplace ' . s:pc_id 252 for key in keys(s:breakpoints) 253 exe 'sign unplace ' . (s:break_id + key) 254 endfor 255 sign undefine debugPC 256 sign undefine debugBreakpoint 257 unlet s:breakpoints 258endfunc 259 260" :Break - Set a breakpoint at the cursor position. 261func s:SetBreakpoint() 262 " Setting a breakpoint may not work while the program is running. 263 " Interrupt to make it work. 264 let do_continue = 0 265 if !s:stopped 266 let do_continue = 1 267 call s:SendCommand('-exec-interrupt') 268 sleep 10m 269 endif 270 call s:SendCommand('-break-insert --source ' 271 \ . fnameescape(expand('%:p')) . ' --line ' . line('.')) 272 if do_continue 273 call s:SendCommand('-exec-continue') 274 endif 275endfunc 276 277" :Delete - Delete a breakpoint at the cursor position. 278func s:DeleteBreakpoint() 279 let fname = fnameescape(expand('%:p')) 280 let lnum = line('.') 281 for [key, val] in items(s:breakpoints) 282 if val['fname'] == fname && val['lnum'] == lnum 283 call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r") 284 " Assume this always wors, the reply is simply "^done". 285 exe 'sign unplace ' . (s:break_id + key) 286 unlet s:breakpoints[key] 287 break 288 endif 289 endfor 290endfunc 291 292" :Next, :Continue, etc - send a command to gdb 293func s:SendCommand(cmd) 294 call term_sendkeys(s:commbuf, a:cmd . "\r") 295endfunc 296 297func s:Run(args) 298 if a:args != '' 299 call s:SendCommand('-exec-arguments ' . a:args) 300 endif 301 call s:SendCommand('-exec-run') 302endfunc 303 304func s:SendEval(expr) 305 call s:SendCommand('-data-evaluate-expression "' . a:expr . '"') 306 let s:evalexpr = a:expr 307endfunc 308 309" :Evaluate - evaluate what is under the cursor 310func s:Evaluate(range, arg) 311 if a:arg != '' 312 let expr = a:arg 313 elseif a:range == 2 314 let pos = getcurpos() 315 let reg = getreg('v', 1, 1) 316 let regt = getregtype('v') 317 normal! gv"vy 318 let expr = @v 319 call setpos('.', pos) 320 call setreg('v', reg, regt) 321 else 322 let expr = expand('<cexpr>') 323 endif 324 call s:SendEval(expr) 325endfunc 326 327let s:evalFromBalloonExpr = 0 328 329" Handle the result of data-evaluate-expression 330func s:HandleEvaluate(msg) 331 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 332 let value = substitute(value, '\\"', '"', 'g') 333 if s:evalFromBalloonExpr 334 if s:evalFromBalloonExprResult == '' 335 let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value 336 else 337 let s:evalFromBalloonExprResult .= ' = ' . value 338 endif 339 call balloon_show(s:evalFromBalloonExprResult) 340 else 341 echomsg '"' . s:evalexpr . '": ' . value 342 endif 343 344 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 345 " Looks like a pointer, also display what it points to. 346 call s:SendEval('*' . s:evalexpr) 347 else 348 let s:evalFromBalloonExpr = 0 349 endif 350endfunc 351 352" Show a balloon with information of the variable under the mouse pointer, 353" if there is any. 354func TermDebugBalloonExpr() 355 if v:beval_winid != s:startwin 356 return 357 endif 358 call s:SendEval(v:beval_text) 359 let s:evalFromBalloonExpr = 1 360 let s:evalFromBalloonExprResult = '' 361 return '' 362endfunc 363 364" Handle an error. 365func s:HandleError(msg) 366 if a:msg =~ 'No symbol .* in current context' 367 \ || a:msg =~ 'Cannot access memory at address ' 368 \ || a:msg =~ 'Attempt to use a type name as an expression' 369 " Result of s:SendEval() failed, ignore. 370 return 371 endif 372 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 373endfunc 374 375" Handle stopping and running message from gdb. 376" Will update the sign that shows the current position. 377func s:HandleCursor(msg) 378 let wid = win_getid(winnr()) 379 380 if a:msg =~ '^\*stopped' 381 let s:stopped = 1 382 elseif a:msg =~ '^\*running' 383 let s:stopped = 0 384 endif 385 386 if win_gotoid(s:startwin) 387 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 388 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 389 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 390 if lnum =~ '^[0-9]*$' 391 if expand('%:p') != fnamemodify(fname, ':p') 392 if &modified 393 " TODO: find existing window 394 exe 'split ' . fnameescape(fname) 395 let s:startwin = win_getid(winnr()) 396 else 397 exe 'edit ' . fnameescape(fname) 398 endif 399 endif 400 exe lnum 401 exe 'sign unplace ' . s:pc_id 402 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname 403 setlocal signcolumn=yes 404 endif 405 else 406 exe 'sign unplace ' . s:pc_id 407 endif 408 409 call win_gotoid(wid) 410 endif 411endfunc 412 413" Handle setting a breakpoint 414" Will update the sign that shows the breakpoint 415func s:HandleNewBreakpoint(msg) 416 let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0 417 if nr == 0 418 return 419 endif 420 421 if has_key(s:breakpoints, nr) 422 let entry = s:breakpoints[nr] 423 else 424 let entry = {} 425 let s:breakpoints[nr] = entry 426 endif 427 428 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 429 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 430 let entry['fname'] = fname 431 let entry['lnum'] = lnum 432 433 if bufloaded(fname) 434 call s:PlaceSign(nr, entry) 435 endif 436endfunc 437 438func s:PlaceSign(nr, entry) 439 exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname'] 440 let a:entry['placed'] = 1 441endfunc 442 443" Handle deleting a breakpoint 444" Will remove the sign that shows the breakpoint 445func s:HandleBreakpointDelete(msg) 446 let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 447 if nr == 0 448 return 449 endif 450 if has_key(s:breakpoints, nr) 451 let entry = s:breakpoints[nr] 452 if has_key(entry, 'placed') 453 exe 'sign unplace ' . (s:break_id + nr) 454 unlet entry['placed'] 455 endif 456 unlet s:breakpoints[nr] 457 endif 458endfunc 459 460" Handle a BufRead autocommand event: place any signs. 461func s:BufRead() 462 let fname = expand('<afile>:p') 463 for [nr, entry] in items(s:breakpoints) 464 if entry['fname'] == fname 465 call s:PlaceSign(nr, entry) 466 endif 467 endfor 468endfunc 469 470" Handle a BufUnloaded autocommand event: unplace any signs. 471func s:BufUnloaded() 472 let fname = expand('<afile>:p') 473 for [nr, entry] in items(s:breakpoints) 474 if entry['fname'] == fname 475 let entry['placed'] = 0 476 endif 477 endfor 478endfunc 479 480