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