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 -bang Termdebug call s:StartDebug(<bang>0, <f-args>)
29command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>)
30
31" Name of the gdb command, defaults to "gdb".
32if !exists('termdebugger')
33  let termdebugger = 'gdb'
34endif
35
36let s:pc_id = 12
37let s:break_id = 13
38let s:stopped = 1
39
40if &background == 'light'
41  hi default debugPC term=reverse ctermbg=lightblue guibg=lightblue
42else
43  hi default debugPC term=reverse ctermbg=darkblue guibg=darkblue
44endif
45hi default debugBreakpoint term=reverse ctermbg=red guibg=red
46
47func s:StartDebug(bang, ...)
48  " First argument is the command to debug, second core file or process ID.
49  call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang})
50endfunc
51
52func s:StartDebugCommand(bang, ...)
53  " First argument is the command to debug, rest are run arguments.
54  call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang})
55endfunc
56
57func s:StartDebug_internal(dict)
58  if exists('s:gdbwin')
59    echoerr 'Terminal debugger already running'
60    return
61  endif
62
63  let s:startwin = win_getid(winnr())
64  let s:startsigncolumn = &signcolumn
65
66  let s:save_columns = 0
67  if exists('g:termdebug_wide')
68    if &columns < g:termdebug_wide
69      let s:save_columns = &columns
70      let &columns = g:termdebug_wide
71    endif
72    let vertical = 1
73  else
74    let vertical = 0
75  endif
76
77  " Open a terminal window without a job, to run the debugged program
78  let s:ptybuf = term_start('NONE', {
79	\ 'term_name': 'gdb program',
80	\ 'vertical': vertical,
81	\ })
82  if s:ptybuf == 0
83    echoerr 'Failed to open the program terminal window'
84    return
85  endif
86  let pty = job_info(term_getjob(s:ptybuf))['tty_out']
87  let s:ptywin = win_getid(winnr())
88  if vertical
89    " Assuming the source code window will get a signcolumn, use two more
90    " columns for that, thus one less for the terminal window.
91    exe (&columns / 2 - 1) . "wincmd |"
92  endif
93
94  " Create a hidden terminal window to communicate with gdb
95  let s:commbuf = term_start('NONE', {
96	\ 'term_name': 'gdb communication',
97	\ 'out_cb': function('s:CommOutput'),
98	\ 'hidden': 1,
99	\ })
100  if s:commbuf == 0
101    echoerr 'Failed to open the communication terminal window'
102    exe 'bwipe! ' . s:ptybuf
103    return
104  endif
105  let commpty = job_info(term_getjob(s:commbuf))['tty_out']
106
107  " Open a terminal window to run the debugger.
108  " Add -quiet to avoid the intro message causing a hit-enter prompt.
109  let gdb_args = get(a:dict, 'gdb_args', [])
110  let proc_args = get(a:dict, 'proc_args', [])
111
112  let cmd = [g:termdebugger, '-quiet', '-tty', pty] + gdb_args
113  echomsg 'executing "' . join(cmd) . '"'
114  let s:gdbbuf = term_start(cmd, {
115	\ 'exit_cb': function('s:EndDebug'),
116	\ 'term_finish': 'close',
117	\ })
118  if s:gdbbuf == 0
119    echoerr 'Failed to open the gdb terminal window'
120    exe 'bwipe! ' . s:ptybuf
121    exe 'bwipe! ' . s:commbuf
122    return
123  endif
124  let s:gdbwin = win_getid(winnr())
125
126  " Set arguments to be run
127  if len(proc_args)
128    call term_sendkeys(s:gdbbuf, 'set args ' . join(proc_args) . "\r")
129  endif
130
131  " Connect gdb to the communication pty, using the GDB/MI interface
132  call term_sendkeys(s:gdbbuf, 'new-ui mi ' . commpty . "\r")
133
134  " Wait for the response to show up, users may not notice the error and wonder
135  " why the debugger doesn't work.
136  let try_count = 0
137  while 1
138    let response = ''
139    for lnum in range(1,200)
140      if term_getline(s:gdbbuf, lnum) =~ 'new-ui mi '
141	let response = term_getline(s:gdbbuf, lnum + 1)
142	if response =~ 'Undefined command'
143	  echoerr 'Sorry, your gdb is too old, gdb 7.12 is required'
144	  exe 'bwipe! ' . s:ptybuf
145	  exe 'bwipe! ' . s:commbuf
146	  return
147	endif
148	if response =~ 'New UI allocated'
149	  " Success!
150	  break
151	endif
152      endif
153    endfor
154    if response =~ 'New UI allocated'
155      break
156    endif
157    let try_count += 1
158    if try_count > 100
159      echoerr 'Cannot check if your gdb works, continuing anyway'
160      break
161    endif
162    sleep 10m
163  endwhile
164
165  " Interpret commands while the target is running.  This should usualy only be
166  " exec-interrupt, since many commands don't work properly while the target is
167  " running.
168  call s:SendCommand('-gdb-set mi-async on')
169
170  " Disable pagination, it causes everything to stop at the gdb
171  " "Type <return> to continue" prompt.
172  call s:SendCommand('-gdb-set pagination off')
173
174  " Sign used to highlight the line where the program has stopped.
175  " There can be only one.
176  sign define debugPC linehl=debugPC
177
178  " Sign used to indicate a breakpoint.
179  " Can be used multiple times.
180  sign define debugBreakpoint text=>> texthl=debugBreakpoint
181
182  " Install debugger commands in the text window.
183  call win_gotoid(s:startwin)
184  call s:InstallCommands()
185  call win_gotoid(s:gdbwin)
186
187  " Enable showing a balloon with eval info
188  if has("balloon_eval") || has("balloon_eval_term")
189    set balloonexpr=TermDebugBalloonExpr()
190    if has("balloon_eval")
191      set ballooneval
192    endif
193    if has("balloon_eval_term")
194      set balloonevalterm
195    endif
196  endif
197
198  let s:breakpoints = {}
199
200  augroup TermDebug
201    au BufRead * call s:BufRead()
202    au BufUnload * call s:BufUnloaded()
203  augroup END
204
205  " Run the command if the bang attribute was given
206  " and got to the window
207  if get(a:dict, 'bang', 0)
208    call s:SendCommand('-exec-run')
209    call win_gotoid(s:ptywin)
210  endif
211
212endfunc
213
214func s:EndDebug(job, status)
215  exe 'bwipe! ' . s:ptybuf
216  exe 'bwipe! ' . s:commbuf
217  unlet s:gdbwin
218
219  let curwinid = win_getid(winnr())
220
221  call win_gotoid(s:startwin)
222  let &signcolumn = s:startsigncolumn
223  call s:DeleteCommands()
224
225  call win_gotoid(curwinid)
226  if s:save_columns > 0
227    let &columns = s:save_columns
228  endif
229
230  if has("balloon_eval") || has("balloon_eval_term")
231    set balloonexpr=
232    if has("balloon_eval")
233      set noballooneval
234    endif
235    if has("balloon_eval_term")
236      set noballoonevalterm
237    endif
238  endif
239
240  au! TermDebug
241endfunc
242
243" Handle a message received from gdb on the GDB/MI interface.
244func s:CommOutput(chan, msg)
245  let msgs = split(a:msg, "\r")
246
247  for msg in msgs
248    " remove prefixed NL
249    if msg[0] == "\n"
250      let msg = msg[1:]
251    endif
252    if msg != ''
253      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
254	call s:HandleCursor(msg)
255      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
256	call s:HandleNewBreakpoint(msg)
257      elseif msg =~ '^=breakpoint-deleted,'
258	call s:HandleBreakpointDelete(msg)
259      elseif msg =~ '^\^done,value='
260	call s:HandleEvaluate(msg)
261      elseif msg =~ '^\^error,msg='
262	call s:HandleError(msg)
263      endif
264    endif
265  endfor
266endfunc
267
268" Install commands in the current window to control the debugger.
269func s:InstallCommands()
270  command Break call s:SetBreakpoint()
271  command Clear call s:ClearBreakpoint()
272  command Step call s:SendCommand('-exec-step')
273  command Over call s:SendCommand('-exec-next')
274  command Finish call s:SendCommand('-exec-finish')
275  command -nargs=* Run call s:Run(<q-args>)
276  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
277  command Stop call s:SendCommand('-exec-interrupt')
278  command Continue call s:SendCommand('-exec-continue')
279  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
280  command Gdb call win_gotoid(s:gdbwin)
281  command Program call win_gotoid(s:ptywin)
282  command Source call s:GotoStartwinOrCreateIt()
283  command Winbar call s:InstallWinbar()
284
285  " TODO: can the K mapping be restored?
286  nnoremap K :Evaluate<CR>
287
288  if has('menu') && &mouse != ''
289    call s:InstallWinbar()
290
291    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
292      let s:saved_mousemodel = &mousemodel
293      let &mousemodel = 'popup_setpos'
294      an 1.200 PopUp.-SEP3-	<Nop>
295      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
296      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
297      an 1.230 PopUp.Evaluate		:Evaluate<CR>
298    endif
299  endif
300endfunc
301
302let s:winbar_winids = []
303
304" Install the window toolbar in the current window.
305func s:InstallWinbar()
306  if has('menu') && &mouse != ''
307    nnoremenu WinBar.Step   :Step<CR>
308    nnoremenu WinBar.Next   :Over<CR>
309    nnoremenu WinBar.Finish :Finish<CR>
310    nnoremenu WinBar.Cont   :Continue<CR>
311    nnoremenu WinBar.Stop   :Stop<CR>
312    nnoremenu WinBar.Eval   :Evaluate<CR>
313    call add(s:winbar_winids, win_getid(winnr()))
314  endif
315endfunc
316
317" Delete installed debugger commands in the current window.
318func s:DeleteCommands()
319  delcommand Break
320  delcommand Clear
321  delcommand Step
322  delcommand Over
323  delcommand Finish
324  delcommand Run
325  delcommand Arguments
326  delcommand Stop
327  delcommand Continue
328  delcommand Evaluate
329  delcommand Gdb
330  delcommand Program
331  delcommand Source
332  delcommand Winbar
333
334  nunmap K
335
336  if has('menu')
337    " Remove the WinBar entries from all windows where it was added.
338    let curwinid = win_getid(winnr())
339    for winid in s:winbar_winids
340      if win_gotoid(winid)
341	aunmenu WinBar.Step
342	aunmenu WinBar.Next
343	aunmenu WinBar.Finish
344	aunmenu WinBar.Cont
345	aunmenu WinBar.Stop
346	aunmenu WinBar.Eval
347      endif
348    endfor
349    call win_gotoid(curwinid)
350    let s:winbar_winids = []
351
352    if exists('s:saved_mousemodel')
353      let &mousemodel = s:saved_mousemodel
354      unlet s:saved_mousemodel
355      aunmenu PopUp.-SEP3-
356      aunmenu PopUp.Set\ breakpoint
357      aunmenu PopUp.Clear\ breakpoint
358      aunmenu PopUp.Evaluate
359    endif
360  endif
361
362  exe 'sign unplace ' . s:pc_id
363  for key in keys(s:breakpoints)
364    exe 'sign unplace ' . (s:break_id + key)
365  endfor
366  sign undefine debugPC
367  sign undefine debugBreakpoint
368  unlet s:breakpoints
369endfunc
370
371" :Break - Set a breakpoint at the cursor position.
372func s:SetBreakpoint()
373  " Setting a breakpoint may not work while the program is running.
374  " Interrupt to make it work.
375  let do_continue = 0
376  if !s:stopped
377    let do_continue = 1
378    call s:SendCommand('-exec-interrupt')
379    sleep 10m
380  endif
381  call s:SendCommand('-break-insert --source '
382	\ . fnameescape(expand('%:p')) . ' --line ' . line('.'))
383  if do_continue
384    call s:SendCommand('-exec-continue')
385  endif
386endfunc
387
388" :Clear - Delete a breakpoint at the cursor position.
389func s:ClearBreakpoint()
390  let fname = fnameescape(expand('%:p'))
391  let lnum = line('.')
392  for [key, val] in items(s:breakpoints)
393    if val['fname'] == fname && val['lnum'] == lnum
394      call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r")
395      " Assume this always wors, the reply is simply "^done".
396      exe 'sign unplace ' . (s:break_id + key)
397      unlet s:breakpoints[key]
398      break
399    endif
400  endfor
401endfunc
402
403" :Next, :Continue, etc - send a command to gdb
404func s:SendCommand(cmd)
405  call term_sendkeys(s:commbuf, a:cmd . "\r")
406endfunc
407
408func s:Run(args)
409  if a:args != ''
410    call s:SendCommand('-exec-arguments ' . a:args)
411  endif
412  call s:SendCommand('-exec-run')
413endfunc
414
415func s:SendEval(expr)
416  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
417  let s:evalexpr = a:expr
418endfunc
419
420" :Evaluate - evaluate what is under the cursor
421func s:Evaluate(range, arg)
422  if a:arg != ''
423    let expr = a:arg
424  elseif a:range == 2
425    let pos = getcurpos()
426    let reg = getreg('v', 1, 1)
427    let regt = getregtype('v')
428    normal! gv"vy
429    let expr = @v
430    call setpos('.', pos)
431    call setreg('v', reg, regt)
432  else
433    let expr = expand('<cexpr>')
434  endif
435  let s:ignoreEvalError = 0
436  call s:SendEval(expr)
437endfunc
438
439let s:ignoreEvalError = 0
440let s:evalFromBalloonExpr = 0
441
442" Handle the result of data-evaluate-expression
443func s:HandleEvaluate(msg)
444  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
445  let value = substitute(value, '\\"', '"', 'g')
446  if s:evalFromBalloonExpr
447    if s:evalFromBalloonExprResult == ''
448      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
449    else
450      let s:evalFromBalloonExprResult .= ' = ' . value
451    endif
452    call balloon_show(s:evalFromBalloonExprResult)
453  else
454    echomsg '"' . s:evalexpr . '": ' . value
455  endif
456
457  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
458    " Looks like a pointer, also display what it points to.
459    let s:ignoreEvalError = 1
460    call s:SendEval('*' . s:evalexpr)
461  else
462    let s:evalFromBalloonExpr = 0
463  endif
464endfunc
465
466" Show a balloon with information of the variable under the mouse pointer,
467" if there is any.
468func TermDebugBalloonExpr()
469  if v:beval_winid != s:startwin
470    return
471  endif
472  let s:evalFromBalloonExpr = 1
473  let s:evalFromBalloonExprResult = ''
474  let s:ignoreEvalError = 1
475  call s:SendEval(v:beval_text)
476  return ''
477endfunc
478
479" Handle an error.
480func s:HandleError(msg)
481  if s:ignoreEvalError
482    " Result of s:SendEval() failed, ignore.
483    let s:ignoreEvalError = 0
484    let s:evalFromBalloonExpr = 0
485    return
486  endif
487  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
488endfunc
489
490func s:GotoStartwinOrCreateIt()
491  if !win_gotoid(s:startwin)
492    new
493    let s:startwin = win_getid(winnr())
494    call s:InstallWinbar()
495  endif
496endfunc
497
498" Handle stopping and running message from gdb.
499" Will update the sign that shows the current position.
500func s:HandleCursor(msg)
501  let wid = win_getid(winnr())
502
503  if a:msg =~ '^\*stopped'
504    let s:stopped = 1
505  elseif a:msg =~ '^\*running'
506    let s:stopped = 0
507  endif
508
509  call s:GotoStartwinOrCreateIt()
510
511  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
512  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
513    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
514    if lnum =~ '^[0-9]*$'
515      if expand('%:p') != fnamemodify(fname, ':p')
516	if &modified
517	  " TODO: find existing window
518	  exe 'split ' . fnameescape(fname)
519	  let s:startwin = win_getid(winnr())
520	  call s:InstallWinbar()
521	else
522	  exe 'edit ' . fnameescape(fname)
523	endif
524      endif
525      exe lnum
526      exe 'sign unplace ' . s:pc_id
527      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
528      setlocal signcolumn=yes
529    endif
530  else
531    exe 'sign unplace ' . s:pc_id
532  endif
533
534  call win_gotoid(wid)
535endfunc
536
537" Handle setting a breakpoint
538" Will update the sign that shows the breakpoint
539func s:HandleNewBreakpoint(msg)
540  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
541  if nr == 0
542    return
543  endif
544
545  if has_key(s:breakpoints, nr)
546    let entry = s:breakpoints[nr]
547  else
548    let entry = {}
549    let s:breakpoints[nr] = entry
550  endif
551
552  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
553  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
554  let entry['fname'] = fname
555  let entry['lnum'] = lnum
556
557  if bufloaded(fname)
558    call s:PlaceSign(nr, entry)
559  endif
560endfunc
561
562func s:PlaceSign(nr, entry)
563  exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname']
564  let a:entry['placed'] = 1
565endfunc
566
567" Handle deleting a breakpoint
568" Will remove the sign that shows the breakpoint
569func s:HandleBreakpointDelete(msg)
570  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
571  if nr == 0
572    return
573  endif
574  if has_key(s:breakpoints, nr)
575    let entry = s:breakpoints[nr]
576    if has_key(entry, 'placed')
577      exe 'sign unplace ' . (s:break_id + nr)
578      unlet entry['placed']
579    endif
580    unlet s:breakpoints[nr]
581  endif
582endfunc
583
584" Handle a BufRead autocommand event: place any signs.
585func s:BufRead()
586  let fname = expand('<afile>:p')
587  for [nr, entry] in items(s:breakpoints)
588    if entry['fname'] == fname
589      call s:PlaceSign(nr, entry)
590    endif
591  endfor
592endfunc
593
594" Handle a BufUnloaded autocommand event: unplace any signs.
595func s:BufUnloaded()
596  let fname = expand('<afile>:p')
597  for [nr, entry] in items(s:breakpoints)
598    if entry['fname'] == fname
599      let entry['placed'] = 0
600    endif
601  endfor
602endfunc
603
604