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 Winbar call s:InstallWinbar() 250 251 " TODO: can the K mapping be restored? 252 nnoremap K :Evaluate<CR> 253 254 if has('menu') && &mouse != '' 255 call s:InstallWinbar() 256 257 if !exists('g:termdebug_popup') || g:termdebug_popup != 0 258 let s:saved_mousemodel = &mousemodel 259 let &mousemodel = 'popup_setpos' 260 an 1.200 PopUp.-SEP3- <Nop> 261 an 1.210 PopUp.Set\ breakpoint :Break<CR> 262 an 1.220 PopUp.Clear\ breakpoint :Clear<CR> 263 an 1.230 PopUp.Evaluate :Evaluate<CR> 264 endif 265 endif 266endfunc 267 268let s:winbar_winids = [] 269 270" Install the window toolbar in the current window. 271func s:InstallWinbar() 272 nnoremenu WinBar.Step :Step<CR> 273 nnoremenu WinBar.Next :Over<CR> 274 nnoremenu WinBar.Finish :Finish<CR> 275 nnoremenu WinBar.Cont :Continue<CR> 276 nnoremenu WinBar.Stop :Stop<CR> 277 nnoremenu WinBar.Eval :Evaluate<CR> 278 call add(s:winbar_winids, win_getid(winnr())) 279endfunc 280 281" Delete installed debugger commands in the current window. 282func s:DeleteCommands() 283 delcommand Break 284 delcommand Clear 285 delcommand Step 286 delcommand Over 287 delcommand Finish 288 delcommand Run 289 delcommand Arguments 290 delcommand Stop 291 delcommand Continue 292 delcommand Evaluate 293 delcommand Gdb 294 delcommand Program 295 delcommand Winbar 296 297 nunmap K 298 299 if has('menu') 300 " Remove the WinBar entries from all windows where it was added. 301 let curwinid = win_getid(winnr()) 302 for winid in s:winbar_winids 303 if win_gotoid(winid) 304 aunmenu WinBar.Step 305 aunmenu WinBar.Next 306 aunmenu WinBar.Finish 307 aunmenu WinBar.Cont 308 aunmenu WinBar.Stop 309 aunmenu WinBar.Eval 310 endif 311 endfor 312 call win_gotoid(curwinid) 313 let s:winbar_winids = [] 314 315 if exists('s:saved_mousemodel') 316 let &mousemodel = s:saved_mousemodel 317 unlet s:saved_mousemodel 318 aunmenu PopUp.-SEP3- 319 aunmenu PopUp.Set\ breakpoint 320 aunmenu PopUp.Clear\ breakpoint 321 aunmenu PopUp.Evaluate 322 endif 323 endif 324 325 exe 'sign unplace ' . s:pc_id 326 for key in keys(s:breakpoints) 327 exe 'sign unplace ' . (s:break_id + key) 328 endfor 329 sign undefine debugPC 330 sign undefine debugBreakpoint 331 unlet s:breakpoints 332endfunc 333 334" :Break - Set a breakpoint at the cursor position. 335func s:SetBreakpoint() 336 " Setting a breakpoint may not work while the program is running. 337 " Interrupt to make it work. 338 let do_continue = 0 339 if !s:stopped 340 let do_continue = 1 341 call s:SendCommand('-exec-interrupt') 342 sleep 10m 343 endif 344 call s:SendCommand('-break-insert --source ' 345 \ . fnameescape(expand('%:p')) . ' --line ' . line('.')) 346 if do_continue 347 call s:SendCommand('-exec-continue') 348 endif 349endfunc 350 351" :Clear - Delete a breakpoint at the cursor position. 352func s:ClearBreakpoint() 353 let fname = fnameescape(expand('%:p')) 354 let lnum = line('.') 355 for [key, val] in items(s:breakpoints) 356 if val['fname'] == fname && val['lnum'] == lnum 357 call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r") 358 " Assume this always wors, the reply is simply "^done". 359 exe 'sign unplace ' . (s:break_id + key) 360 unlet s:breakpoints[key] 361 break 362 endif 363 endfor 364endfunc 365 366" :Next, :Continue, etc - send a command to gdb 367func s:SendCommand(cmd) 368 call term_sendkeys(s:commbuf, a:cmd . "\r") 369endfunc 370 371func s:Run(args) 372 if a:args != '' 373 call s:SendCommand('-exec-arguments ' . a:args) 374 endif 375 call s:SendCommand('-exec-run') 376endfunc 377 378func s:SendEval(expr) 379 call s:SendCommand('-data-evaluate-expression "' . a:expr . '"') 380 let s:evalexpr = a:expr 381endfunc 382 383" :Evaluate - evaluate what is under the cursor 384func s:Evaluate(range, arg) 385 if a:arg != '' 386 let expr = a:arg 387 elseif a:range == 2 388 let pos = getcurpos() 389 let reg = getreg('v', 1, 1) 390 let regt = getregtype('v') 391 normal! gv"vy 392 let expr = @v 393 call setpos('.', pos) 394 call setreg('v', reg, regt) 395 else 396 let expr = expand('<cexpr>') 397 endif 398 let s:ignoreEvalError = 0 399 call s:SendEval(expr) 400endfunc 401 402let s:ignoreEvalError = 0 403let s:evalFromBalloonExpr = 0 404 405" Handle the result of data-evaluate-expression 406func s:HandleEvaluate(msg) 407 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 408 let value = substitute(value, '\\"', '"', 'g') 409 if s:evalFromBalloonExpr 410 if s:evalFromBalloonExprResult == '' 411 let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value 412 else 413 let s:evalFromBalloonExprResult .= ' = ' . value 414 endif 415 call balloon_show(s:evalFromBalloonExprResult) 416 else 417 echomsg '"' . s:evalexpr . '": ' . value 418 endif 419 420 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 421 " Looks like a pointer, also display what it points to. 422 let s:ignoreEvalError = 1 423 call s:SendEval('*' . s:evalexpr) 424 else 425 let s:evalFromBalloonExpr = 0 426 endif 427endfunc 428 429" Show a balloon with information of the variable under the mouse pointer, 430" if there is any. 431func TermDebugBalloonExpr() 432 if v:beval_winid != s:startwin 433 return 434 endif 435 let s:evalFromBalloonExpr = 1 436 let s:evalFromBalloonExprResult = '' 437 let s:ignoreEvalError = 1 438 call s:SendEval(v:beval_text) 439 return '' 440endfunc 441 442" Handle an error. 443func s:HandleError(msg) 444 if s:ignoreEvalError 445 " Result of s:SendEval() failed, ignore. 446 let s:ignoreEvalError = 0 447 let s:evalFromBalloonExpr = 0 448 return 449 endif 450 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 451endfunc 452 453" Handle stopping and running message from gdb. 454" Will update the sign that shows the current position. 455func s:HandleCursor(msg) 456 let wid = win_getid(winnr()) 457 458 if a:msg =~ '^\*stopped' 459 let s:stopped = 1 460 elseif a:msg =~ '^\*running' 461 let s:stopped = 0 462 endif 463 464 if win_gotoid(s:startwin) 465 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 466 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 467 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 468 if lnum =~ '^[0-9]*$' 469 if expand('%:p') != fnamemodify(fname, ':p') 470 if &modified 471 " TODO: find existing window 472 exe 'split ' . fnameescape(fname) 473 let s:startwin = win_getid(winnr()) 474 else 475 exe 'edit ' . fnameescape(fname) 476 endif 477 endif 478 exe lnum 479 exe 'sign unplace ' . s:pc_id 480 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname 481 setlocal signcolumn=yes 482 endif 483 else 484 exe 'sign unplace ' . s:pc_id 485 endif 486 487 call win_gotoid(wid) 488 endif 489endfunc 490 491" Handle setting a breakpoint 492" Will update the sign that shows the breakpoint 493func s:HandleNewBreakpoint(msg) 494 let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0 495 if nr == 0 496 return 497 endif 498 499 if has_key(s:breakpoints, nr) 500 let entry = s:breakpoints[nr] 501 else 502 let entry = {} 503 let s:breakpoints[nr] = entry 504 endif 505 506 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 507 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 508 let entry['fname'] = fname 509 let entry['lnum'] = lnum 510 511 if bufloaded(fname) 512 call s:PlaceSign(nr, entry) 513 endif 514endfunc 515 516func s:PlaceSign(nr, entry) 517 exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname'] 518 let a:entry['placed'] = 1 519endfunc 520 521" Handle deleting a breakpoint 522" Will remove the sign that shows the breakpoint 523func s:HandleBreakpointDelete(msg) 524 let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 525 if nr == 0 526 return 527 endif 528 if has_key(s:breakpoints, nr) 529 let entry = s:breakpoints[nr] 530 if has_key(entry, 'placed') 531 exe 'sign unplace ' . (s:break_id + nr) 532 unlet entry['placed'] 533 endif 534 unlet s:breakpoints[nr] 535 endif 536endfunc 537 538" Handle a BufRead autocommand event: place any signs. 539func s:BufRead() 540 let fname = expand('<afile>:p') 541 for [nr, entry] in items(s:breakpoints) 542 if entry['fname'] == fname 543 call s:PlaceSign(nr, entry) 544 endif 545 endfor 546endfunc 547 548" Handle a BufUnloaded autocommand event: unplace any signs. 549func s:BufUnloaded() 550 let fname = expand('<afile>:p') 551 for [nr, entry] in items(s:breakpoints) 552 if entry['fname'] == fname 553 let entry['placed'] = 0 554 endif 555 endfor 556endfunc 557 558