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 -bang Termdebug call s:StartDebug(<bang>0, <f-args>) 29command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>) 30 31" Name of the gdb command, defaults to "gdb". 32if !exists('termdebugger') 33 let termdebugger = 'gdb' 34endif 35 36let s:pc_id = 12 37let s:break_id = 13 38let s:stopped = 1 39 40if &background == 'light' 41 hi default debugPC term=reverse ctermbg=lightblue guibg=lightblue 42else 43 hi default debugPC term=reverse ctermbg=darkblue guibg=darkblue 44endif 45hi default debugBreakpoint term=reverse ctermbg=red guibg=red 46 47func s:StartDebug(bang, ...) 48 " First argument is the command to debug, second core file or process ID. 49 call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang}) 50endfunc 51 52func s:StartDebugCommand(bang, ...) 53 " First argument is the command to debug, rest are run arguments. 54 call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang}) 55endfunc 56 57func s:StartDebug_internal(dict) 58 if exists('s:gdbwin') 59 echoerr 'Terminal debugger already running' 60 return 61 endif 62 63 let s:startwin = win_getid(winnr()) 64 let s:startsigncolumn = &signcolumn 65 66 let s:save_columns = 0 67 if exists('g:termdebug_wide') 68 if &columns < g:termdebug_wide 69 let s:save_columns = &columns 70 let &columns = g:termdebug_wide 71 endif 72 let vertical = 1 73 else 74 let vertical = 0 75 endif 76 77 " Open a terminal window without a job, to run the debugged program 78 let s:ptybuf = term_start('NONE', { 79 \ 'term_name': 'gdb program', 80 \ 'vertical': vertical, 81 \ }) 82 if s:ptybuf == 0 83 echoerr 'Failed to open the program terminal window' 84 return 85 endif 86 let pty = job_info(term_getjob(s:ptybuf))['tty_out'] 87 let s:ptywin = win_getid(winnr()) 88 if vertical 89 " Assuming the source code window will get a signcolumn, use two more 90 " columns for that, thus one less for the terminal window. 91 exe (&columns / 2 - 1) . "wincmd |" 92 endif 93 94 " Create a hidden terminal window to communicate with gdb 95 let s:commbuf = term_start('NONE', { 96 \ 'term_name': 'gdb communication', 97 \ 'out_cb': function('s:CommOutput'), 98 \ 'hidden': 1, 99 \ }) 100 if s:commbuf == 0 101 echoerr 'Failed to open the communication terminal window' 102 exe 'bwipe! ' . s:ptybuf 103 return 104 endif 105 let commpty = job_info(term_getjob(s:commbuf))['tty_out'] 106 107 " Open a terminal window to run the debugger. 108 " Add -quiet to avoid the intro message causing a hit-enter prompt. 109 let gdb_args = get(a:dict, 'gdb_args', []) 110 let proc_args = get(a:dict, 'proc_args', []) 111 112 let cmd = [g:termdebugger, '-quiet', '-tty', pty] + gdb_args 113 echomsg 'executing "' . join(cmd) . '"' 114 let s:gdbbuf = term_start(cmd, { 115 \ 'exit_cb': function('s:EndDebug'), 116 \ 'term_finish': 'close', 117 \ }) 118 if s:gdbbuf == 0 119 echoerr 'Failed to open the gdb terminal window' 120 exe 'bwipe! ' . s:ptybuf 121 exe 'bwipe! ' . s:commbuf 122 return 123 endif 124 let s:gdbwin = win_getid(winnr()) 125 126 " Set arguments to be run 127 if len(proc_args) 128 call term_sendkeys(s:gdbbuf, 'set args ' . join(proc_args) . "\r") 129 endif 130 131 " Connect gdb to the communication pty, using the GDB/MI interface 132 call term_sendkeys(s:gdbbuf, 'new-ui mi ' . commpty . "\r") 133 134 " Wait for the response to show up, users may not notice the error and wonder 135 " why the debugger doesn't work. 136 let try_count = 0 137 while 1 138 let response = '' 139 for lnum in range(1,200) 140 if term_getline(s:gdbbuf, lnum) =~ 'new-ui mi ' 141 let response = term_getline(s:gdbbuf, lnum + 1) 142 if response =~ 'Undefined command' 143 echoerr 'Sorry, your gdb is too old, gdb 7.12 is required' 144 exe 'bwipe! ' . s:ptybuf 145 exe 'bwipe! ' . s:commbuf 146 return 147 endif 148 if response =~ 'New UI allocated' 149 " Success! 150 break 151 endif 152 endif 153 endfor 154 if response =~ 'New UI allocated' 155 break 156 endif 157 let try_count += 1 158 if try_count > 100 159 echoerr 'Cannot check if your gdb works, continuing anyway' 160 break 161 endif 162 sleep 10m 163 endwhile 164 165 " Interpret commands while the target is running. This should usualy only be 166 " exec-interrupt, since many commands don't work properly while the target is 167 " running. 168 call s:SendCommand('-gdb-set mi-async on') 169 170 " Disable pagination, it causes everything to stop at the gdb 171 " "Type <return> to continue" prompt. 172 call s:SendCommand('-gdb-set pagination off') 173 174 " Sign used to highlight the line where the program has stopped. 175 " There can be only one. 176 sign define debugPC linehl=debugPC 177 178 " Sign used to indicate a breakpoint. 179 " Can be used multiple times. 180 sign define debugBreakpoint text=>> texthl=debugBreakpoint 181 182 " Install debugger commands in the text window. 183 call win_gotoid(s:startwin) 184 call s:InstallCommands() 185 call win_gotoid(s:gdbwin) 186 187 " Enable showing a balloon with eval info 188 if has("balloon_eval") || has("balloon_eval_term") 189 set balloonexpr=TermDebugBalloonExpr() 190 if has("balloon_eval") 191 set ballooneval 192 endif 193 if has("balloon_eval_term") 194 set balloonevalterm 195 endif 196 endif 197 198 let s:breakpoints = {} 199 200 augroup TermDebug 201 au BufRead * call s:BufRead() 202 au BufUnload * call s:BufUnloaded() 203 augroup END 204 205 " Run the command if the bang attribute was given 206 " and got to the window 207 if get(a:dict, 'bang', 0) 208 call s:SendCommand('-exec-run') 209 call win_gotoid(s:ptywin) 210 endif 211 212endfunc 213 214func s:EndDebug(job, status) 215 exe 'bwipe! ' . s:ptybuf 216 exe 'bwipe! ' . s:commbuf 217 unlet s:gdbwin 218 219 let curwinid = win_getid(winnr()) 220 221 call win_gotoid(s:startwin) 222 let &signcolumn = s:startsigncolumn 223 call s:DeleteCommands() 224 225 call win_gotoid(curwinid) 226 if s:save_columns > 0 227 let &columns = s:save_columns 228 endif 229 230 if has("balloon_eval") || has("balloon_eval_term") 231 set balloonexpr= 232 if has("balloon_eval") 233 set noballooneval 234 endif 235 if has("balloon_eval_term") 236 set noballoonevalterm 237 endif 238 endif 239 240 au! TermDebug 241endfunc 242 243" Handle a message received from gdb on the GDB/MI interface. 244func s:CommOutput(chan, msg) 245 let msgs = split(a:msg, "\r") 246 247 for msg in msgs 248 " remove prefixed NL 249 if msg[0] == "\n" 250 let msg = msg[1:] 251 endif 252 if msg != '' 253 if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)' 254 call s:HandleCursor(msg) 255 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 256 call s:HandleNewBreakpoint(msg) 257 elseif msg =~ '^=breakpoint-deleted,' 258 call s:HandleBreakpointDelete(msg) 259 elseif msg =~ '^\^done,value=' 260 call s:HandleEvaluate(msg) 261 elseif msg =~ '^\^error,msg=' 262 call s:HandleError(msg) 263 endif 264 endif 265 endfor 266endfunc 267 268" Install commands in the current window to control the debugger. 269func s:InstallCommands() 270 command Break call s:SetBreakpoint() 271 command Clear call s:ClearBreakpoint() 272 command Step call s:SendCommand('-exec-step') 273 command Over call s:SendCommand('-exec-next') 274 command Finish call s:SendCommand('-exec-finish') 275 command -nargs=* Run call s:Run(<q-args>) 276 command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>) 277 command Stop call s:SendCommand('-exec-interrupt') 278 command Continue call s:SendCommand('-exec-continue') 279 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 280 command Gdb call win_gotoid(s:gdbwin) 281 command Program call win_gotoid(s:ptywin) 282 command Source call s:GotoStartwinOrCreateIt() 283 command Winbar call s:InstallWinbar() 284 285 " TODO: can the K mapping be restored? 286 nnoremap K :Evaluate<CR> 287 288 if has('menu') && &mouse != '' 289 call s:InstallWinbar() 290 291 if !exists('g:termdebug_popup') || g:termdebug_popup != 0 292 let s:saved_mousemodel = &mousemodel 293 let &mousemodel = 'popup_setpos' 294 an 1.200 PopUp.-SEP3- <Nop> 295 an 1.210 PopUp.Set\ breakpoint :Break<CR> 296 an 1.220 PopUp.Clear\ breakpoint :Clear<CR> 297 an 1.230 PopUp.Evaluate :Evaluate<CR> 298 endif 299 endif 300endfunc 301 302let s:winbar_winids = [] 303 304" Install the window toolbar in the current window. 305func s:InstallWinbar() 306 if has('menu') && &mouse != '' 307 nnoremenu WinBar.Step :Step<CR> 308 nnoremenu WinBar.Next :Over<CR> 309 nnoremenu WinBar.Finish :Finish<CR> 310 nnoremenu WinBar.Cont :Continue<CR> 311 nnoremenu WinBar.Stop :Stop<CR> 312 nnoremenu WinBar.Eval :Evaluate<CR> 313 call add(s:winbar_winids, win_getid(winnr())) 314 endif 315endfunc 316 317" Delete installed debugger commands in the current window. 318func s:DeleteCommands() 319 delcommand Break 320 delcommand Clear 321 delcommand Step 322 delcommand Over 323 delcommand Finish 324 delcommand Run 325 delcommand Arguments 326 delcommand Stop 327 delcommand Continue 328 delcommand Evaluate 329 delcommand Gdb 330 delcommand Program 331 delcommand Source 332 delcommand Winbar 333 334 nunmap K 335 336 if has('menu') 337 " Remove the WinBar entries from all windows where it was added. 338 let curwinid = win_getid(winnr()) 339 for winid in s:winbar_winids 340 if win_gotoid(winid) 341 aunmenu WinBar.Step 342 aunmenu WinBar.Next 343 aunmenu WinBar.Finish 344 aunmenu WinBar.Cont 345 aunmenu WinBar.Stop 346 aunmenu WinBar.Eval 347 endif 348 endfor 349 call win_gotoid(curwinid) 350 let s:winbar_winids = [] 351 352 if exists('s:saved_mousemodel') 353 let &mousemodel = s:saved_mousemodel 354 unlet s:saved_mousemodel 355 aunmenu PopUp.-SEP3- 356 aunmenu PopUp.Set\ breakpoint 357 aunmenu PopUp.Clear\ breakpoint 358 aunmenu PopUp.Evaluate 359 endif 360 endif 361 362 exe 'sign unplace ' . s:pc_id 363 for key in keys(s:breakpoints) 364 exe 'sign unplace ' . (s:break_id + key) 365 endfor 366 sign undefine debugPC 367 sign undefine debugBreakpoint 368 unlet s:breakpoints 369endfunc 370 371" :Break - Set a breakpoint at the cursor position. 372func s:SetBreakpoint() 373 " Setting a breakpoint may not work while the program is running. 374 " Interrupt to make it work. 375 let do_continue = 0 376 if !s:stopped 377 let do_continue = 1 378 call s:SendCommand('-exec-interrupt') 379 sleep 10m 380 endif 381 call s:SendCommand('-break-insert --source ' 382 \ . fnameescape(expand('%:p')) . ' --line ' . line('.')) 383 if do_continue 384 call s:SendCommand('-exec-continue') 385 endif 386endfunc 387 388" :Clear - Delete a breakpoint at the cursor position. 389func s:ClearBreakpoint() 390 let fname = fnameescape(expand('%:p')) 391 let lnum = line('.') 392 for [key, val] in items(s:breakpoints) 393 if val['fname'] == fname && val['lnum'] == lnum 394 call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r") 395 " Assume this always wors, the reply is simply "^done". 396 exe 'sign unplace ' . (s:break_id + key) 397 unlet s:breakpoints[key] 398 break 399 endif 400 endfor 401endfunc 402 403" :Next, :Continue, etc - send a command to gdb 404func s:SendCommand(cmd) 405 call term_sendkeys(s:commbuf, a:cmd . "\r") 406endfunc 407 408func s:Run(args) 409 if a:args != '' 410 call s:SendCommand('-exec-arguments ' . a:args) 411 endif 412 call s:SendCommand('-exec-run') 413endfunc 414 415func s:SendEval(expr) 416 call s:SendCommand('-data-evaluate-expression "' . a:expr . '"') 417 let s:evalexpr = a:expr 418endfunc 419 420" :Evaluate - evaluate what is under the cursor 421func s:Evaluate(range, arg) 422 if a:arg != '' 423 let expr = a:arg 424 elseif a:range == 2 425 let pos = getcurpos() 426 let reg = getreg('v', 1, 1) 427 let regt = getregtype('v') 428 normal! gv"vy 429 let expr = @v 430 call setpos('.', pos) 431 call setreg('v', reg, regt) 432 else 433 let expr = expand('<cexpr>') 434 endif 435 let s:ignoreEvalError = 0 436 call s:SendEval(expr) 437endfunc 438 439let s:ignoreEvalError = 0 440let s:evalFromBalloonExpr = 0 441 442" Handle the result of data-evaluate-expression 443func s:HandleEvaluate(msg) 444 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 445 let value = substitute(value, '\\"', '"', 'g') 446 if s:evalFromBalloonExpr 447 if s:evalFromBalloonExprResult == '' 448 let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value 449 else 450 let s:evalFromBalloonExprResult .= ' = ' . value 451 endif 452 call balloon_show(s:evalFromBalloonExprResult) 453 else 454 echomsg '"' . s:evalexpr . '": ' . value 455 endif 456 457 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 458 " Looks like a pointer, also display what it points to. 459 let s:ignoreEvalError = 1 460 call s:SendEval('*' . s:evalexpr) 461 else 462 let s:evalFromBalloonExpr = 0 463 endif 464endfunc 465 466" Show a balloon with information of the variable under the mouse pointer, 467" if there is any. 468func TermDebugBalloonExpr() 469 if v:beval_winid != s:startwin 470 return 471 endif 472 let s:evalFromBalloonExpr = 1 473 let s:evalFromBalloonExprResult = '' 474 let s:ignoreEvalError = 1 475 call s:SendEval(v:beval_text) 476 return '' 477endfunc 478 479" Handle an error. 480func s:HandleError(msg) 481 if s:ignoreEvalError 482 " Result of s:SendEval() failed, ignore. 483 let s:ignoreEvalError = 0 484 let s:evalFromBalloonExpr = 0 485 return 486 endif 487 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 488endfunc 489 490func s:GotoStartwinOrCreateIt() 491 if !win_gotoid(s:startwin) 492 new 493 let s:startwin = win_getid(winnr()) 494 call s:InstallWinbar() 495 endif 496endfunc 497 498" Handle stopping and running message from gdb. 499" Will update the sign that shows the current position. 500func s:HandleCursor(msg) 501 let wid = win_getid(winnr()) 502 503 if a:msg =~ '^\*stopped' 504 let s:stopped = 1 505 elseif a:msg =~ '^\*running' 506 let s:stopped = 0 507 endif 508 509 call s:GotoStartwinOrCreateIt() 510 511 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 512 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 513 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 514 if lnum =~ '^[0-9]*$' 515 if expand('%:p') != fnamemodify(fname, ':p') 516 if &modified 517 " TODO: find existing window 518 exe 'split ' . fnameescape(fname) 519 let s:startwin = win_getid(winnr()) 520 call s:InstallWinbar() 521 else 522 exe 'edit ' . fnameescape(fname) 523 endif 524 endif 525 exe lnum 526 exe 'sign unplace ' . s:pc_id 527 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname 528 setlocal signcolumn=yes 529 endif 530 else 531 exe 'sign unplace ' . s:pc_id 532 endif 533 534 call win_gotoid(wid) 535endfunc 536 537" Handle setting a breakpoint 538" Will update the sign that shows the breakpoint 539func s:HandleNewBreakpoint(msg) 540 let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0 541 if nr == 0 542 return 543 endif 544 545 if has_key(s:breakpoints, nr) 546 let entry = s:breakpoints[nr] 547 else 548 let entry = {} 549 let s:breakpoints[nr] = entry 550 endif 551 552 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 553 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 554 let entry['fname'] = fname 555 let entry['lnum'] = lnum 556 557 if bufloaded(fname) 558 call s:PlaceSign(nr, entry) 559 endif 560endfunc 561 562func s:PlaceSign(nr, entry) 563 exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname'] 564 let a:entry['placed'] = 1 565endfunc 566 567" Handle deleting a breakpoint 568" Will remove the sign that shows the breakpoint 569func s:HandleBreakpointDelete(msg) 570 let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 571 if nr == 0 572 return 573 endif 574 if has_key(s:breakpoints, nr) 575 let entry = s:breakpoints[nr] 576 if has_key(entry, 'placed') 577 exe 'sign unplace ' . (s:break_id + nr) 578 unlet entry['placed'] 579 endif 580 unlet s:breakpoints[nr] 581 endif 582endfunc 583 584" Handle a BufRead autocommand event: place any signs. 585func s:BufRead() 586 let fname = expand('<afile>:p') 587 for [nr, entry] in items(s:breakpoints) 588 if entry['fname'] == fname 589 call s:PlaceSign(nr, entry) 590 endif 591 endfor 592endfunc 593 594" Handle a BufUnloaded autocommand event: unplace any signs. 595func s:BufUnloaded() 596 let fname = expand('<afile>:p') 597 for [nr, entry] in items(s:breakpoints) 598 if entry['fname'] == fname 599 let entry['placed'] = 0 600 endif 601 endfor 602endfunc 603 604