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 'Sorry, your gdb is too old, gdb 7.12 is required'
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  " Disable pagination, it causes everything to stop at the gdb
147  " "Type <return> to continue" prompt.
148  call s:SendCommand('-gdb-set pagination off')
149
150  " Sign used to highlight the line where the program has stopped.
151  " There can be only one.
152  sign define debugPC linehl=debugPC
153
154  " Sign used to indicate a breakpoint.
155  " Can be used multiple times.
156  sign define debugBreakpoint text=>> texthl=debugBreakpoint
157
158  " Install debugger commands in the text window.
159  call win_gotoid(s:startwin)
160  call s:InstallCommands()
161  call win_gotoid(s:gdbwin)
162
163  " Enable showing a balloon with eval info
164  if has("balloon_eval") || has("balloon_eval_term")
165    set balloonexpr=TermDebugBalloonExpr()
166    if has("balloon_eval")
167      set ballooneval
168    endif
169    if has("balloon_eval_term")
170      set balloonevalterm
171    endif
172  endif
173
174  let s:breakpoints = {}
175
176  augroup TermDebug
177    au BufRead * call s:BufRead()
178    au BufUnload * call s:BufUnloaded()
179  augroup END
180endfunc
181
182func s:EndDebug(job, status)
183  exe 'bwipe! ' . s:ptybuf
184  exe 'bwipe! ' . s:commbuf
185
186  let curwinid = win_getid(winnr())
187
188  call win_gotoid(s:startwin)
189  let &signcolumn = s:startsigncolumn
190  call s:DeleteCommands()
191
192  call win_gotoid(curwinid)
193  if s:save_columns > 0
194    let &columns = s:save_columns
195  endif
196
197  if has("balloon_eval") || has("balloon_eval_term")
198    set balloonexpr=
199    if has("balloon_eval")
200      set noballooneval
201    endif
202    if has("balloon_eval_term")
203      set noballoonevalterm
204    endif
205  endif
206
207  au! TermDebug
208endfunc
209
210" Handle a message received from gdb on the GDB/MI interface.
211func s:CommOutput(chan, msg)
212  let msgs = split(a:msg, "\r")
213
214  for msg in msgs
215    " remove prefixed NL
216    if msg[0] == "\n"
217      let msg = msg[1:]
218    endif
219    if msg != ''
220      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
221	call s:HandleCursor(msg)
222      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
223	call s:HandleNewBreakpoint(msg)
224      elseif msg =~ '^=breakpoint-deleted,'
225	call s:HandleBreakpointDelete(msg)
226      elseif msg =~ '^\^done,value='
227	call s:HandleEvaluate(msg)
228      elseif msg =~ '^\^error,msg='
229	call s:HandleError(msg)
230      endif
231    endif
232  endfor
233endfunc
234
235" Install commands in the current window to control the debugger.
236func s:InstallCommands()
237  command Break call s:SetBreakpoint()
238  command Clear call s:ClearBreakpoint()
239  command Step call s:SendCommand('-exec-step')
240  command Over call s:SendCommand('-exec-next')
241  command Finish call s:SendCommand('-exec-finish')
242  command -nargs=* Run call s:Run(<q-args>)
243  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
244  command Stop call s:SendCommand('-exec-interrupt')
245  command Continue call s:SendCommand('-exec-continue')
246  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
247  command Gdb call win_gotoid(s:gdbwin)
248  command Program call win_gotoid(s:ptywin)
249  command Winbar call s:InstallWinbar()
250
251  " TODO: can the K mapping be restored?
252  nnoremap K :Evaluate<CR>
253
254  if has('menu') && &mouse != ''
255    call s:InstallWinbar()
256
257    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
258      let s:saved_mousemodel = &mousemodel
259      let &mousemodel = 'popup_setpos'
260      an 1.200 PopUp.-SEP3-	<Nop>
261      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
262      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
263      an 1.230 PopUp.Evaluate		:Evaluate<CR>
264    endif
265  endif
266endfunc
267
268let s:winbar_winids = []
269
270" Install the window toolbar in the current window.
271func s:InstallWinbar()
272  nnoremenu WinBar.Step   :Step<CR>
273  nnoremenu WinBar.Next   :Over<CR>
274  nnoremenu WinBar.Finish :Finish<CR>
275  nnoremenu WinBar.Cont   :Continue<CR>
276  nnoremenu WinBar.Stop   :Stop<CR>
277  nnoremenu WinBar.Eval   :Evaluate<CR>
278  call add(s:winbar_winids, win_getid(winnr()))
279endfunc
280
281" Delete installed debugger commands in the current window.
282func s:DeleteCommands()
283  delcommand Break
284  delcommand Clear
285  delcommand Step
286  delcommand Over
287  delcommand Finish
288  delcommand Run
289  delcommand Arguments
290  delcommand Stop
291  delcommand Continue
292  delcommand Evaluate
293  delcommand Gdb
294  delcommand Program
295  delcommand Winbar
296
297  nunmap K
298
299  if has('menu')
300    " Remove the WinBar entries from all windows where it was added.
301    let curwinid = win_getid(winnr())
302    for winid in s:winbar_winids
303      if win_gotoid(winid)
304	aunmenu WinBar.Step
305	aunmenu WinBar.Next
306	aunmenu WinBar.Finish
307	aunmenu WinBar.Cont
308	aunmenu WinBar.Stop
309	aunmenu WinBar.Eval
310      endif
311    endfor
312    call win_gotoid(curwinid)
313    let s:winbar_winids = []
314
315    if exists('s:saved_mousemodel')
316      let &mousemodel = s:saved_mousemodel
317      unlet s:saved_mousemodel
318      aunmenu PopUp.-SEP3-
319      aunmenu PopUp.Set\ breakpoint
320      aunmenu PopUp.Clear\ breakpoint
321      aunmenu PopUp.Evaluate
322    endif
323  endif
324
325  exe 'sign unplace ' . s:pc_id
326  for key in keys(s:breakpoints)
327    exe 'sign unplace ' . (s:break_id + key)
328  endfor
329  sign undefine debugPC
330  sign undefine debugBreakpoint
331  unlet s:breakpoints
332endfunc
333
334" :Break - Set a breakpoint at the cursor position.
335func s:SetBreakpoint()
336  " Setting a breakpoint may not work while the program is running.
337  " Interrupt to make it work.
338  let do_continue = 0
339  if !s:stopped
340    let do_continue = 1
341    call s:SendCommand('-exec-interrupt')
342    sleep 10m
343  endif
344  call s:SendCommand('-break-insert --source '
345	\ . fnameescape(expand('%:p')) . ' --line ' . line('.'))
346  if do_continue
347    call s:SendCommand('-exec-continue')
348  endif
349endfunc
350
351" :Clear - Delete a breakpoint at the cursor position.
352func s:ClearBreakpoint()
353  let fname = fnameescape(expand('%:p'))
354  let lnum = line('.')
355  for [key, val] in items(s:breakpoints)
356    if val['fname'] == fname && val['lnum'] == lnum
357      call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r")
358      " Assume this always wors, the reply is simply "^done".
359      exe 'sign unplace ' . (s:break_id + key)
360      unlet s:breakpoints[key]
361      break
362    endif
363  endfor
364endfunc
365
366" :Next, :Continue, etc - send a command to gdb
367func s:SendCommand(cmd)
368  call term_sendkeys(s:commbuf, a:cmd . "\r")
369endfunc
370
371func s:Run(args)
372  if a:args != ''
373    call s:SendCommand('-exec-arguments ' . a:args)
374  endif
375  call s:SendCommand('-exec-run')
376endfunc
377
378func s:SendEval(expr)
379  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
380  let s:evalexpr = a:expr
381endfunc
382
383" :Evaluate - evaluate what is under the cursor
384func s:Evaluate(range, arg)
385  if a:arg != ''
386    let expr = a:arg
387  elseif a:range == 2
388    let pos = getcurpos()
389    let reg = getreg('v', 1, 1)
390    let regt = getregtype('v')
391    normal! gv"vy
392    let expr = @v
393    call setpos('.', pos)
394    call setreg('v', reg, regt)
395  else
396    let expr = expand('<cexpr>')
397  endif
398  let s:ignoreEvalError = 0
399  call s:SendEval(expr)
400endfunc
401
402let s:ignoreEvalError = 0
403let s:evalFromBalloonExpr = 0
404
405" Handle the result of data-evaluate-expression
406func s:HandleEvaluate(msg)
407  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
408  let value = substitute(value, '\\"', '"', 'g')
409  if s:evalFromBalloonExpr
410    if s:evalFromBalloonExprResult == ''
411      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
412    else
413      let s:evalFromBalloonExprResult .= ' = ' . value
414    endif
415    call balloon_show(s:evalFromBalloonExprResult)
416  else
417    echomsg '"' . s:evalexpr . '": ' . value
418  endif
419
420  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
421    " Looks like a pointer, also display what it points to.
422    let s:ignoreEvalError = 1
423    call s:SendEval('*' . s:evalexpr)
424  else
425    let s:evalFromBalloonExpr = 0
426  endif
427endfunc
428
429" Show a balloon with information of the variable under the mouse pointer,
430" if there is any.
431func TermDebugBalloonExpr()
432  if v:beval_winid != s:startwin
433    return
434  endif
435  let s:evalFromBalloonExpr = 1
436  let s:evalFromBalloonExprResult = ''
437  let s:ignoreEvalError = 1
438  call s:SendEval(v:beval_text)
439  return ''
440endfunc
441
442" Handle an error.
443func s:HandleError(msg)
444  if s:ignoreEvalError
445    " Result of s:SendEval() failed, ignore.
446    let s:ignoreEvalError = 0
447    let s:evalFromBalloonExpr = 0
448    return
449  endif
450  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
451endfunc
452
453" Handle stopping and running message from gdb.
454" Will update the sign that shows the current position.
455func s:HandleCursor(msg)
456  let wid = win_getid(winnr())
457
458  if a:msg =~ '^\*stopped'
459    let s:stopped = 1
460  elseif a:msg =~ '^\*running'
461    let s:stopped = 0
462  endif
463
464  if win_gotoid(s:startwin)
465    let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
466    if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
467      let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
468      if lnum =~ '^[0-9]*$'
469	if expand('%:p') != fnamemodify(fname, ':p')
470	  if &modified
471	    " TODO: find existing window
472	    exe 'split ' . fnameescape(fname)
473	    let s:startwin = win_getid(winnr())
474	  else
475	    exe 'edit ' . fnameescape(fname)
476	  endif
477	endif
478	exe lnum
479	exe 'sign unplace ' . s:pc_id
480	exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
481	setlocal signcolumn=yes
482      endif
483    else
484      exe 'sign unplace ' . s:pc_id
485    endif
486
487    call win_gotoid(wid)
488  endif
489endfunc
490
491" Handle setting a breakpoint
492" Will update the sign that shows the breakpoint
493func s:HandleNewBreakpoint(msg)
494  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
495  if nr == 0
496    return
497  endif
498
499  if has_key(s:breakpoints, nr)
500    let entry = s:breakpoints[nr]
501  else
502    let entry = {}
503    let s:breakpoints[nr] = entry
504  endif
505
506  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
507  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
508  let entry['fname'] = fname
509  let entry['lnum'] = lnum
510
511  if bufloaded(fname)
512    call s:PlaceSign(nr, entry)
513  endif
514endfunc
515
516func s:PlaceSign(nr, entry)
517  exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname']
518  let a:entry['placed'] = 1
519endfunc
520
521" Handle deleting a breakpoint
522" Will remove the sign that shows the breakpoint
523func s:HandleBreakpointDelete(msg)
524  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
525  if nr == 0
526    return
527  endif
528  if has_key(s:breakpoints, nr)
529    let entry = s:breakpoints[nr]
530    if has_key(entry, 'placed')
531      exe 'sign unplace ' . (s:break_id + nr)
532      unlet entry['placed']
533    endif
534    unlet s:breakpoints[nr]
535  endif
536endfunc
537
538" Handle a BufRead autocommand event: place any signs.
539func s:BufRead()
540  let fname = expand('<afile>:p')
541  for [nr, entry] in items(s:breakpoints)
542    if entry['fname'] == fname
543      call s:PlaceSign(nr, entry)
544    endif
545  endfor
546endfunc
547
548" Handle a BufUnloaded autocommand event: unplace any signs.
549func s:BufUnloaded()
550  let fname = expand('<afile>:p')
551  for [nr, entry] in items(s:breakpoints)
552    if entry['fname'] == fname
553      let entry['placed'] = 0
554    endif
555  endfor
556endfunc
557
558