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