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" The command that starts debugging, e.g. ":Termdebug vim". 24" To end type "quit" in the gdb window. 25command -nargs=* -complete=file Termdebug call s:StartDebug(<q-args>) 26 27" Name of the gdb command, defaults to "gdb". 28if !exists('termdebugger') 29 let termdebugger = 'gdb' 30endif 31 32let s:pc_id = 12 33let s:break_id = 13 34 35if &background == 'light' 36 hi default debugPC term=reverse ctermbg=lightblue guibg=lightblue 37else 38 hi default debugPC term=reverse ctermbg=darkblue guibg=darkblue 39endif 40hi default debugBreakpoint term=reverse ctermbg=red guibg=red 41 42func s:StartDebug(cmd) 43 let s:startwin = win_getid(winnr()) 44 let s:startsigncolumn = &signcolumn 45 46 if exists('g:termdebug_wide') && &columns < g:termdebug_wide 47 let s:save_columns = &columns 48 let &columns = g:termdebug_wide 49 let vertical = 1 50 else 51 let s:save_columns = 0 52 let vertical = 0 53 endif 54 55 " Open a terminal window without a job, to run the debugged program 56 let s:ptybuf = term_start('NONE', { 57 \ 'term_name': 'gdb program', 58 \ 'vertical': vertical, 59 \ }) 60 if s:ptybuf == 0 61 echoerr 'Failed to open the program terminal window' 62 return 63 endif 64 let pty = job_info(term_getjob(s:ptybuf))['tty_out'] 65 let s:ptywin = win_getid(winnr()) 66 67 " Create a hidden terminal window to communicate with gdb 68 let s:commbuf = term_start('NONE', { 69 \ 'term_name': 'gdb communication', 70 \ 'out_cb': function('s:CommOutput'), 71 \ 'hidden': 1, 72 \ }) 73 if s:commbuf == 0 74 echoerr 'Failed to open the communication terminal window' 75 exe 'bwipe! ' . s:ptybuf 76 return 77 endif 78 let commpty = job_info(term_getjob(s:commbuf))['tty_out'] 79 80 " Open a terminal window to run the debugger. 81 let cmd = [g:termdebugger, '-tty', pty, a:cmd] 82 echomsg 'executing "' . join(cmd) . '"' 83 let gdbbuf = term_start(cmd, { 84 \ 'exit_cb': function('s:EndDebug'), 85 \ 'term_finish': 'close', 86 \ }) 87 if gdbbuf == 0 88 echoerr 'Failed to open the gdb terminal window' 89 exe 'bwipe! ' . s:ptybuf 90 exe 'bwipe! ' . s:commbuf 91 return 92 endif 93 let s:gdbwin = win_getid(winnr()) 94 95 " Connect gdb to the communication pty, using the GDB/MI interface 96 call term_sendkeys(gdbbuf, 'new-ui mi ' . commpty . "\r") 97 98 " Sign used to highlight the line where the program has stopped. 99 " There can be only one. 100 sign define debugPC linehl=debugPC 101 102 " Sign used to indicate a breakpoint. 103 " Can be used multiple times. 104 sign define debugBreakpoint text=>> texthl=debugBreakpoint 105 106 " Install debugger commands in the text window. 107 call win_gotoid(s:startwin) 108 call s:InstallCommands() 109 call win_gotoid(s:gdbwin) 110 111 let s:breakpoints = {} 112 113 augroup TermDebug 114 au BufRead * call s:BufRead() 115 au BufUnload * call s:BufUnloaded() 116 augroup END 117endfunc 118 119func s:EndDebug(job, status) 120 exe 'bwipe! ' . s:ptybuf 121 exe 'bwipe! ' . s:commbuf 122 123 let curwinid = win_getid(winnr()) 124 125 call win_gotoid(s:startwin) 126 let &signcolumn = s:startsigncolumn 127 call s:DeleteCommands() 128 129 call win_gotoid(curwinid) 130 if s:save_columns > 0 131 let &columns = s:save_columns 132 endif 133 134 au! TermDebug 135endfunc 136 137" Handle a message received from gdb on the GDB/MI interface. 138func s:CommOutput(chan, msg) 139 let msgs = split(a:msg, "\r") 140 141 for msg in msgs 142 " remove prefixed NL 143 if msg[0] == "\n" 144 let msg = msg[1:] 145 endif 146 if msg != '' 147 if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)' 148 call s:HandleCursor(msg) 149 elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,' 150 call s:HandleNewBreakpoint(msg) 151 elseif msg =~ '^=breakpoint-deleted,' 152 call s:HandleBreakpointDelete(msg) 153 elseif msg =~ '^\^done,value=' 154 call s:HandleEvaluate(msg) 155 elseif msg =~ '^\^error,msg=' 156 call s:HandleError(msg) 157 endif 158 endif 159 endfor 160endfunc 161 162" Install commands in the current window to control the debugger. 163func s:InstallCommands() 164 command Break call s:SetBreakpoint() 165 command Delete call s:DeleteBreakpoint() 166 command Step call s:SendCommand('-exec-step') 167 command Over call s:SendCommand('-exec-next') 168 command Finish call s:SendCommand('-exec-finish') 169 command Continue call s:SendCommand('-exec-continue') 170 command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) 171 command Gdb call win_gotoid(s:gdbwin) 172 command Program call win_gotoid(s:ptywin) 173 174 " TODO: can the K mapping be restored? 175 nnoremap K :Evaluate<CR> 176 177 if has('menu') 178 amenu WinBar.Step :Step<CR> 179 amenu WinBar.Next :Over<CR> 180 amenu WinBar.Finish :Finish<CR> 181 amenu WinBar.Cont :Continue<CR> 182 amenu WinBar.Eval :Evaluate<CR> 183 endif 184endfunc 185 186" Delete installed debugger commands in the current window. 187func s:DeleteCommands() 188 delcommand Break 189 delcommand Delete 190 delcommand Step 191 delcommand Over 192 delcommand Finish 193 delcommand Continue 194 delcommand Evaluate 195 delcommand Gdb 196 delcommand Program 197 198 nunmap K 199 200 if has('menu') 201 aunmenu WinBar.Step 202 aunmenu WinBar.Next 203 aunmenu WinBar.Finish 204 aunmenu WinBar.Cont 205 aunmenu WinBar.Eval 206 endif 207 208 exe 'sign unplace ' . s:pc_id 209 for key in keys(s:breakpoints) 210 exe 'sign unplace ' . (s:break_id + key) 211 endfor 212 sign undefine debugPC 213 sign undefine debugBreakpoint 214 unlet s:breakpoints 215endfunc 216 217" :Break - Set a breakpoint at the cursor position. 218func s:SetBreakpoint() 219 call term_sendkeys(s:commbuf, '-break-insert --source ' 220 \ . fnameescape(expand('%:p')) . ' --line ' . line('.') . "\r") 221endfunc 222 223" :Delete - Delete a breakpoint at the cursor position. 224func s:DeleteBreakpoint() 225 let fname = fnameescape(expand('%:p')) 226 let lnum = line('.') 227 for [key, val] in items(s:breakpoints) 228 if val['fname'] == fname && val['lnum'] == lnum 229 call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r") 230 " Assume this always wors, the reply is simply "^done". 231 exe 'sign unplace ' . (s:break_id + key) 232 unlet s:breakpoints[key] 233 break 234 endif 235 endfor 236endfunc 237 238" :Next, :Continue, etc - send a command to gdb 239func s:SendCommand(cmd) 240 call term_sendkeys(s:commbuf, a:cmd . "\r") 241endfunc 242 243" :Evaluate - evaluate what is under the cursor 244func s:Evaluate(range, arg) 245 if a:arg != '' 246 let expr = a:arg 247 elseif a:range == 2 248 let pos = getcurpos() 249 let reg = getreg('v', 1, 1) 250 let regt = getregtype('v') 251 normal! gv"vy 252 let expr = @v 253 call setpos('.', pos) 254 call setreg('v', reg, regt) 255 else 256 let expr = expand('<cexpr>') 257 endif 258 call term_sendkeys(s:commbuf, '-data-evaluate-expression "' . expr . "\"\r") 259 let s:evalexpr = expr 260endfunc 261 262" Handle the result of data-evaluate-expression 263func s:HandleEvaluate(msg) 264 let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '') 265 let value = substitute(value, '\\"', '"', 'g') 266 echomsg '"' . s:evalexpr . '": ' . value 267 268 if s:evalexpr[0] != '*' && value =~ '^0x' && value !~ '"$' 269 " Looks like a pointer, also display what it points to. 270 let s:evalexpr = '*' . s:evalexpr 271 call term_sendkeys(s:commbuf, '-data-evaluate-expression "' . s:evalexpr . "\"\r") 272 endif 273endfunc 274 275" Handle an error. 276func s:HandleError(msg) 277 echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '') 278endfunc 279 280" Handle stopping and running message from gdb. 281" Will update the sign that shows the current position. 282func s:HandleCursor(msg) 283 let wid = win_getid(winnr()) 284 285 if win_gotoid(s:startwin) 286 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 287 if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) 288 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 289 if lnum =~ '^[0-9]*$' 290 if expand('%:p') != fnamemodify(fname, ':p') 291 if &modified 292 " TODO: find existing window 293 exe 'split ' . fnameescape(fname) 294 let s:startwin = win_getid(winnr()) 295 else 296 exe 'edit ' . fnameescape(fname) 297 endif 298 endif 299 exe lnum 300 exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname 301 setlocal signcolumn=yes 302 endif 303 else 304 exe 'sign unplace ' . s:pc_id 305 endif 306 307 call win_gotoid(wid) 308 endif 309endfunc 310 311" Handle setting a breakpoint 312" Will update the sign that shows the breakpoint 313func s:HandleNewBreakpoint(msg) 314 let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0 315 if nr == 0 316 return 317 endif 318 319 if has_key(s:breakpoints, nr) 320 let entry = s:breakpoints[nr] 321 else 322 let entry = {} 323 let s:breakpoints[nr] = entry 324 endif 325 326 let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '') 327 let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') 328 let entry['fname'] = fname 329 let entry['lnum'] = lnum 330 331 if bufloaded(fname) 332 call s:PlaceSign(nr, entry) 333 endif 334endfunc 335 336func s:PlaceSign(nr, entry) 337 exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname'] 338 let a:entry['placed'] = 1 339endfunc 340 341" Handle deleting a breakpoint 342" Will remove the sign that shows the breakpoint 343func s:HandleBreakpointDelete(msg) 344 let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0 345 if nr == 0 346 return 347 endif 348 if has_key(s:breakpoints, nr) 349 let entry = s:breakpoints[nr] 350 if has_key(entry, 'placed') 351 exe 'sign unplace ' . (s:break_id + nr) 352 unlet entry['placed'] 353 endif 354 unlet s:breakpoints[nr] 355 endif 356endfunc 357 358" Handle a BufRead autocommand event: place any signs. 359func s:BufRead() 360 let fname = expand('<afile>:p') 361 for [nr, entry] in items(s:breakpoints) 362 if entry['fname'] == fname 363 call s:PlaceSign(nr, entry) 364 endif 365 endfor 366endfunc 367 368" Handle a BufUnloaded autocommand event: unplace any signs. 369func s:BufUnloaded() 370 let fname = expand('<afile>:p') 371 for [nr, entry] in items(s:breakpoints) 372 if entry['fname'] == fname 373 let entry['placed'] = 0 374 endif 375 endfor 376endfunc 377 378