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 73 " Create a hidden terminal window to communicate with gdb 74 let s:commbuf = term_start('NONE', { 75 \ 'term_name': 'gdb communication', 76 \ 'out_cb': function('s:CommOutput'), 77 \ 'hidden': 1, 78 \ }) 79 if s:commbuf == 0 80 echoerr 'Failed to open the communication terminal window' 81 exe 'bwipe! ' . s:ptybuf 82 return 83 endif 84 let commpty = job_info(term_getjob(s:commbuf))['tty_out'] 85 86 " Open a terminal window to run the debugger. 87 " Add -quiet to avoid the intro message causing a hit-enter prompt. 88 let cmd = [g:termdebugger, '-quiet', '-tty', pty, a:cmd] 89 echomsg 'executing "' . join(cmd) . '"' 90 let s:gdbbuf = term_start(cmd, { 91 \ 'exit_cb': function('s:EndDebug'), 92 \ 'term_finish': 'close', 93 \ }) 94 if s:gdbbuf == 0 95 echoerr 'Failed to open the gdb terminal window' 96 exe 'bwipe! ' . s:ptybuf 97 exe 'bwipe! ' . s:commbuf 98 return 99 endif 100 let s:gdbwin = win_getid(winnr()) 101 102 " Connect gdb to the communication pty, using the GDB/MI interface 103 " If you get an error "undefined command" your GDB is too old. 104 call term_sendkeys(s:gdbbuf, 'new-ui mi ' . commpty . "\r") 105 106 " Interpret commands while the target is running. This should usualy only be 107 " exec-interrupt, since many commands don't work properly while the target is 108 " running. 109 call s:SendCommand('-gdb-set mi-async on') 110 111 " Sign used to highlight the line where the program has stopped. 112 " There can be only one. 113 sign define debugPC linehl=debugPC 114 115 " Sign used to indicate a breakpoint. 116 " Can be used multiple times. 117 sign define debugBreakpoint text=>> texthl=debugBreakpoint 118 119 " Install debugger commands in the text window. 120 call win_gotoid(s:startwin) 121 call s:InstallCommands() 122 call win_gotoid(s:gdbwin) 123 124 let s:breakpoints = {} 125 126 augroup TermDebug 127 au BufRead * call s:BufRead() 128 au BufUnload * call s:BufUnloaded() 129 augroup END 130endfunc 131 132func s:EndDebug(job, status) 133 exe 'bwipe! ' . s:ptybuf 134 exe 'bwipe! ' . s:commbuf 135 136 let curwinid = win_getid(winnr()) 137 138 call win_gotoid(s:startwin) 139 let &signcolumn = s:startsigncolumn 140 call s:DeleteCommands() 141 142 call win_gotoid(curwinid) 143 if s:save_columns > 0 144 let &columns = s:save_columns 145 endif 146 147 au! TermDebug 148endfunc 149 150" Handle a message received from gdb on the GDB/MI interface. 151func s:CommOutput(chan, msg) 152 let msgs = split(a:msg, "\r") 153 154 for msg in msgs 155 " remove prefixed NL 156 if msg[0] == "\n" 157 let msg = msg[1:] 158 endif 159 if msg != '' 160 if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)' 161 call s:HandleCursor(msg) 162 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 163 call s:HandleNewBreakpoint(msg) 164 elseif msg =~ '^=breakpoint-deleted,' 165 call s:HandleBreakpointDelete(msg) 166 elseif msg =~ '^\^done,value=' 167 call s:HandleEvaluate(msg) 168 elseif msg =~ '^\^error,msg=' 169 call s:HandleError(msg) 170 endif 171 endif 172 endfor 173endfunc 174 175" Install commands in the current window to control the debugger. 176func s:InstallCommands() 177 command Break call s:SetBreakpoint() 178 command Delete call s:DeleteBreakpoint() 179 command Step call s:SendCommand('-exec-step') 180 command Over call s:SendCommand('-exec-next') 181 command Finish call s:SendCommand('-exec-finish') 182 command -nargs=* Run call s:Run(<q-args>) 183 command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>) 184 command Stop call s:SendCommand('-exec-interrupt') 185 command Continue call s:SendCommand('-exec-continue') 186 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 187 command Gdb call win_gotoid(s:gdbwin) 188 command Program call win_gotoid(s:ptywin) 189 190 " TODO: can the K mapping be restored? 191 nnoremap K :Evaluate<CR> 192 193 if has('menu') 194 nnoremenu WinBar.Step :Step<CR> 195 nnoremenu WinBar.Next :Over<CR> 196 nnoremenu WinBar.Finish :Finish<CR> 197 nnoremenu WinBar.Cont :Continue<CR> 198 nnoremenu WinBar.Stop :Stop<CR> 199 nnoremenu WinBar.Eval :Evaluate<CR> 200 endif 201endfunc 202 203" Delete installed debugger commands in the current window. 204func s:DeleteCommands() 205 delcommand Break 206 delcommand Delete 207 delcommand Step 208 delcommand Over 209 delcommand Finish 210 delcommand Run 211 delcommand Arguments 212 delcommand Stop 213 delcommand Continue 214 delcommand Evaluate 215 delcommand Gdb 216 delcommand Program 217 218 nunmap K 219 220 if has('menu') 221 aunmenu WinBar.Step 222 aunmenu WinBar.Next 223 aunmenu WinBar.Finish 224 aunmenu WinBar.Cont 225 aunmenu WinBar.Stop 226 aunmenu WinBar.Eval 227 endif 228 229 exe 'sign unplace ' . s:pc_id 230 for key in keys(s:breakpoints) 231 exe 'sign unplace ' . (s:break_id + key) 232 endfor 233 sign undefine debugPC 234 sign undefine debugBreakpoint 235 unlet s:breakpoints 236endfunc 237 238" :Break - Set a breakpoint at the cursor position. 239func s:SetBreakpoint() 240 " Setting a breakpoint may not work while the program is running. 241 " Interrupt to make it work. 242 let do_continue = 0 243 if !s:stopped 244 let do_continue = 1 245 call s:SendCommand('-exec-interrupt') 246 sleep 10m 247 endif 248 call s:SendCommand('-break-insert --source ' 249 \ . fnameescape(expand('%:p')) . ' --line ' . line('.')) 250 if do_continue 251 call s:SendCommand('-exec-continue') 252 endif 253endfunc 254 255" :Delete - Delete a breakpoint at the cursor position. 256func s:DeleteBreakpoint() 257 let fname = fnameescape(expand('%:p')) 258 let lnum = line('.') 259 for [key, val] in items(s:breakpoints) 260 if val['fname'] == fname && val['lnum'] == lnum 261 call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r") 262 " Assume this always wors, the reply is simply "^done". 263 exe 'sign unplace ' . (s:break_id + key) 264 unlet s:breakpoints[key] 265 break 266 endif 267 endfor 268endfunc 269 270" :Next, :Continue, etc - send a command to gdb 271func s:SendCommand(cmd) 272 call term_sendkeys(s:commbuf, a:cmd . "\r") 273endfunc 274 275func s:Run(args) 276 if a:args != '' 277 call s:SendCommand('-exec-arguments ' . a:args) 278 endif 279 call s:SendCommand('-exec-run') 280endfunc 281 282" :Evaluate - evaluate what is under the cursor 283func s:Evaluate(range, arg) 284 if a:arg != '' 285 let expr = a:arg 286 elseif a:range == 2 287 let pos = getcurpos() 288 let reg = getreg('v', 1, 1) 289 let regt = getregtype('v') 290 normal! gv"vy 291 let expr = @v 292 call setpos('.', pos) 293 call setreg('v', reg, regt) 294 else 295 let expr = expand('<cexpr>') 296 endif 297 call s:SendCommand('-data-evaluate-expression "' . expr . '"') 298 let s:evalexpr = expr 299endfunc 300 301" Handle the result of data-evaluate-expression 302func s:HandleEvaluate(msg) 303 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 304 let value = substitute(value, '\\"', '"', 'g') 305 echomsg '"' . s:evalexpr . '": ' . value 306 307 if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$' 308 " Looks like a pointer, also display what it points to. 309 let s:evalexpr = '*' . s:evalexpr 310 call term_sendkeys(s:commbuf, '-data-evaluate-expression "' . s:evalexpr . "\"\r") 311 endif 312endfunc 313 314" Handle an error. 315func s:HandleError(msg) 316 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 317endfunc 318 319" Handle stopping and running message from gdb. 320" Will update the sign that shows the current position. 321func s:HandleCursor(msg) 322 let wid = win_getid(winnr()) 323 324 if a:msg =~ '^\*stopped' 325 let s:stopped = 1 326 elseif a:msg =~ '^\*running' 327 let s:stopped = 0 328 endif 329 330 if win_gotoid(s:startwin) 331 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 332 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 333 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 334 if lnum =~ '^[0-9]*$' 335 if expand('%:p') != fnamemodify(fname, ':p') 336 if &modified 337 " TODO: find existing window 338 exe 'split ' . fnameescape(fname) 339 let s:startwin = win_getid(winnr()) 340 else 341 exe 'edit ' . fnameescape(fname) 342 endif 343 endif 344 exe lnum 345 exe 'sign unplace ' . s:pc_id 346 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname 347 setlocal signcolumn=yes 348 endif 349 else 350 exe 'sign unplace ' . s:pc_id 351 endif 352 353 call win_gotoid(wid) 354 endif 355endfunc 356 357" Handle setting a breakpoint 358" Will update the sign that shows the breakpoint 359func s:HandleNewBreakpoint(msg) 360 let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0 361 if nr == 0 362 return 363 endif 364 365 if has_key(s:breakpoints, nr) 366 let entry = s:breakpoints[nr] 367 else 368 let entry = {} 369 let s:breakpoints[nr] = entry 370 endif 371 372 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 373 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 374 let entry['fname'] = fname 375 let entry['lnum'] = lnum 376 377 if bufloaded(fname) 378 call s:PlaceSign(nr, entry) 379 endif 380endfunc 381 382func s:PlaceSign(nr, entry) 383 exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname'] 384 let a:entry['placed'] = 1 385endfunc 386 387" Handle deleting a breakpoint 388" Will remove the sign that shows the breakpoint 389func s:HandleBreakpointDelete(msg) 390 let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 391 if nr == 0 392 return 393 endif 394 if has_key(s:breakpoints, nr) 395 let entry = s:breakpoints[nr] 396 if has_key(entry, 'placed') 397 exe 'sign unplace ' . (s:break_id + nr) 398 unlet entry['placed'] 399 endif 400 unlet s:breakpoints[nr] 401 endif 402endfunc 403 404" Handle a BufRead autocommand event: place any signs. 405func s:BufRead() 406 let fname = expand('<afile>:p') 407 for [nr, entry] in items(s:breakpoints) 408 if entry['fname'] == fname 409 call s:PlaceSign(nr, entry) 410 endif 411 endfor 412endfunc 413 414" Handle a BufUnloaded autocommand event: unplace any signs. 415func s:BufUnloaded() 416 let fname = expand('<afile>:p') 417 for [nr, entry] in items(s:breakpoints) 418 if entry['fname'] == fname 419 let entry['placed'] = 0 420 endif 421 endfor 422endfunc 423 424