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