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 Delete call s:DeleteBreakpoint() 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 216 " TODO: can the K mapping be restored? 217 nnoremap K :Evaluate<CR> 218 219 if has('menu') && &mouse != '' 220 nnoremenu WinBar.Step :Step<CR> 221 nnoremenu WinBar.Next :Over<CR> 222 nnoremenu WinBar.Finish :Finish<CR> 223 nnoremenu WinBar.Cont :Continue<CR> 224 nnoremenu WinBar.Stop :Stop<CR> 225 nnoremenu WinBar.Eval :Evaluate<CR> 226 endif 227endfunc 228 229" Delete installed debugger commands in the current window. 230func s:DeleteCommands() 231 delcommand Break 232 delcommand Delete 233 delcommand Step 234 delcommand Over 235 delcommand Finish 236 delcommand Run 237 delcommand Arguments 238 delcommand Stop 239 delcommand Continue 240 delcommand Evaluate 241 delcommand Gdb 242 delcommand Program 243 244 nunmap K 245 246 if has('menu') 247 aunmenu WinBar.Step 248 aunmenu WinBar.Next 249 aunmenu WinBar.Finish 250 aunmenu WinBar.Cont 251 aunmenu WinBar.Stop 252 aunmenu WinBar.Eval 253 endif 254 255 exe 'sign unplace ' . s:pc_id 256 for key in keys(s:breakpoints) 257 exe 'sign unplace ' . (s:break_id + key) 258 endfor 259 sign undefine debugPC 260 sign undefine debugBreakpoint 261 unlet s:breakpoints 262endfunc 263 264" :Break - Set a breakpoint at the cursor position. 265func s:SetBreakpoint() 266 " Setting a breakpoint may not work while the program is running. 267 " Interrupt to make it work. 268 let do_continue = 0 269 if !s:stopped 270 let do_continue = 1 271 call s:SendCommand('-exec-interrupt') 272 sleep 10m 273 endif 274 call s:SendCommand('-break-insert --source ' 275 \ . fnameescape(expand('%:p')) . ' --line ' . line('.')) 276 if do_continue 277 call s:SendCommand('-exec-continue') 278 endif 279endfunc 280 281" :Delete - Delete a breakpoint at the cursor position. 282func s:DeleteBreakpoint() 283 let fname = fnameescape(expand('%:p')) 284 let lnum = line('.') 285 for [key, val] in items(s:breakpoints) 286 if val['fname'] == fname && val['lnum'] == lnum 287 call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r") 288 " Assume this always wors, the reply is simply "^done". 289 exe 'sign unplace ' . (s:break_id + key) 290 unlet s:breakpoints[key] 291 break 292 endif 293 endfor 294endfunc 295 296" :Next, :Continue, etc - send a command to gdb 297func s:SendCommand(cmd) 298 call term_sendkeys(s:commbuf, a:cmd . "\r") 299endfunc 300 301func s:Run(args) 302 if a:args != '' 303 call s:SendCommand('-exec-arguments ' . a:args) 304 endif 305 call s:SendCommand('-exec-run') 306endfunc 307 308func s:SendEval(expr) 309 call s:SendCommand('-data-evaluate-expression "' . a:expr . '"') 310 let s:evalexpr = a:expr 311endfunc 312 313" :Evaluate - evaluate what is under the cursor 314func s:Evaluate(range, arg) 315 if a:arg != '' 316 let expr = a:arg 317 elseif a:range == 2 318 let pos = getcurpos() 319 let reg = getreg('v', 1, 1) 320 let regt = getregtype('v') 321 normal! gv"vy 322 let expr = @v 323 call setpos('.', pos) 324 call setreg('v', reg, regt) 325 else 326 let expr = expand('<cexpr>') 327 endif 328 let s:ignoreEvalError = 0 329 call s:SendEval(expr) 330endfunc 331 332let s:ignoreEvalError = 0 333let s:evalFromBalloonExpr = 0 334 335" Handle the result of data-evaluate-expression 336func s:HandleEvaluate(msg) 337 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 338 let value = substitute(value, '\\"', '"', 'g') 339 if s:evalFromBalloonExpr 340 if s:evalFromBalloonExprResult == '' 341 let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value 342 else 343 let s:evalFromBalloonExprResult .= ' = ' . value 344 endif 345 call balloon_show(s:evalFromBalloonExprResult) 346 else 347 echomsg '"' . s:evalexpr . '": ' . value 348 endif 349 350 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 351 " Looks like a pointer, also display what it points to. 352 let s:ignoreEvalError = 1 353 call s:SendEval('*' . s:evalexpr) 354 else 355 let s:evalFromBalloonExpr = 0 356 endif 357endfunc 358 359" Show a balloon with information of the variable under the mouse pointer, 360" if there is any. 361func TermDebugBalloonExpr() 362 if v:beval_winid != s:startwin 363 return 364 endif 365 let s:evalFromBalloonExpr = 1 366 let s:evalFromBalloonExprResult = '' 367 let s:ignoreEvalError = 1 368 call s:SendEval(v:beval_text) 369 return '' 370endfunc 371 372" Handle an error. 373func s:HandleError(msg) 374 if s:ignoreEvalError 375 " Result of s:SendEval() failed, ignore. 376 let s:ignoreEvalError = 0 377 let s:evalFromBalloonExpr = 0 378 return 379 endif 380 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 381endfunc 382 383" Handle stopping and running message from gdb. 384" Will update the sign that shows the current position. 385func s:HandleCursor(msg) 386 let wid = win_getid(winnr()) 387 388 if a:msg =~ '^\*stopped' 389 let s:stopped = 1 390 elseif a:msg =~ '^\*running' 391 let s:stopped = 0 392 endif 393 394 if win_gotoid(s:startwin) 395 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 396 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 397 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 398 if lnum =~ '^[0-9]*$' 399 if expand('%:p') != fnamemodify(fname, ':p') 400 if &modified 401 " TODO: find existing window 402 exe 'split ' . fnameescape(fname) 403 let s:startwin = win_getid(winnr()) 404 else 405 exe 'edit ' . fnameescape(fname) 406 endif 407 endif 408 exe lnum 409 exe 'sign unplace ' . s:pc_id 410 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname 411 setlocal signcolumn=yes 412 endif 413 else 414 exe 'sign unplace ' . s:pc_id 415 endif 416 417 call win_gotoid(wid) 418 endif 419endfunc 420 421" Handle setting a breakpoint 422" Will update the sign that shows the breakpoint 423func s:HandleNewBreakpoint(msg) 424 let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0 425 if nr == 0 426 return 427 endif 428 429 if has_key(s:breakpoints, nr) 430 let entry = s:breakpoints[nr] 431 else 432 let entry = {} 433 let s:breakpoints[nr] = entry 434 endif 435 436 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 437 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 438 let entry['fname'] = fname 439 let entry['lnum'] = lnum 440 441 if bufloaded(fname) 442 call s:PlaceSign(nr, entry) 443 endif 444endfunc 445 446func s:PlaceSign(nr, entry) 447 exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname'] 448 let a:entry['placed'] = 1 449endfunc 450 451" Handle deleting a breakpoint 452" Will remove the sign that shows the breakpoint 453func s:HandleBreakpointDelete(msg) 454 let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 455 if nr == 0 456 return 457 endif 458 if has_key(s:breakpoints, nr) 459 let entry = s:breakpoints[nr] 460 if has_key(entry, 'placed') 461 exe 'sign unplace ' . (s:break_id + nr) 462 unlet entry['placed'] 463 endif 464 unlet s:breakpoints[nr] 465 endif 466endfunc 467 468" Handle a BufRead autocommand event: place any signs. 469func s:BufRead() 470 let fname = expand('<afile>:p') 471 for [nr, entry] in items(s:breakpoints) 472 if entry['fname'] == fname 473 call s:PlaceSign(nr, entry) 474 endif 475 endfor 476endfunc 477 478" Handle a BufUnloaded autocommand event: unplace any signs. 479func s:BufUnloaded() 480 let fname = expand('<afile>:p') 481 for [nr, entry] in items(s:breakpoints) 482 if entry['fname'] == fname 483 let entry['placed'] = 0 484 endif 485 endfor 486endfunc 487 488