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