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