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