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