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