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