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 Source call s:GotoStartwinOrCreateIt()
250  command Winbar call s:InstallWinbar()
251
252  " TODO: can the K mapping be restored?
253  nnoremap K :Evaluate<CR>
254
255  if has('menu') && &mouse != ''
256    call s:InstallWinbar()
257
258    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
259      let s:saved_mousemodel = &mousemodel
260      let &mousemodel = 'popup_setpos'
261      an 1.200 PopUp.-SEP3-	<Nop>
262      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
263      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
264      an 1.230 PopUp.Evaluate		:Evaluate<CR>
265    endif
266  endif
267endfunc
268
269let s:winbar_winids = []
270
271" Install the window toolbar in the current window.
272func s:InstallWinbar()
273  if has('menu') && &mouse != ''
274    nnoremenu WinBar.Step   :Step<CR>
275    nnoremenu WinBar.Next   :Over<CR>
276    nnoremenu WinBar.Finish :Finish<CR>
277    nnoremenu WinBar.Cont   :Continue<CR>
278    nnoremenu WinBar.Stop   :Stop<CR>
279    nnoremenu WinBar.Eval   :Evaluate<CR>
280    call add(s:winbar_winids, win_getid(winnr()))
281  endif
282endfunc
283
284" Delete installed debugger commands in the current window.
285func s:DeleteCommands()
286  delcommand Break
287  delcommand Clear
288  delcommand Step
289  delcommand Over
290  delcommand Finish
291  delcommand Run
292  delcommand Arguments
293  delcommand Stop
294  delcommand Continue
295  delcommand Evaluate
296  delcommand Gdb
297  delcommand Program
298  delcommand Winbar
299
300  nunmap K
301
302  if has('menu')
303    " Remove the WinBar entries from all windows where it was added.
304    let curwinid = win_getid(winnr())
305    for winid in s:winbar_winids
306      if win_gotoid(winid)
307	aunmenu WinBar.Step
308	aunmenu WinBar.Next
309	aunmenu WinBar.Finish
310	aunmenu WinBar.Cont
311	aunmenu WinBar.Stop
312	aunmenu WinBar.Eval
313      endif
314    endfor
315    call win_gotoid(curwinid)
316    let s:winbar_winids = []
317
318    if exists('s:saved_mousemodel')
319      let &mousemodel = s:saved_mousemodel
320      unlet s:saved_mousemodel
321      aunmenu PopUp.-SEP3-
322      aunmenu PopUp.Set\ breakpoint
323      aunmenu PopUp.Clear\ breakpoint
324      aunmenu PopUp.Evaluate
325    endif
326  endif
327
328  exe 'sign unplace ' . s:pc_id
329  for key in keys(s:breakpoints)
330    exe 'sign unplace ' . (s:break_id + key)
331  endfor
332  sign undefine debugPC
333  sign undefine debugBreakpoint
334  unlet s:breakpoints
335endfunc
336
337" :Break - Set a breakpoint at the cursor position.
338func s:SetBreakpoint()
339  " Setting a breakpoint may not work while the program is running.
340  " Interrupt to make it work.
341  let do_continue = 0
342  if !s:stopped
343    let do_continue = 1
344    call s:SendCommand('-exec-interrupt')
345    sleep 10m
346  endif
347  call s:SendCommand('-break-insert --source '
348	\ . fnameescape(expand('%:p')) . ' --line ' . line('.'))
349  if do_continue
350    call s:SendCommand('-exec-continue')
351  endif
352endfunc
353
354" :Clear - Delete a breakpoint at the cursor position.
355func s:ClearBreakpoint()
356  let fname = fnameescape(expand('%:p'))
357  let lnum = line('.')
358  for [key, val] in items(s:breakpoints)
359    if val['fname'] == fname && val['lnum'] == lnum
360      call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r")
361      " Assume this always wors, the reply is simply "^done".
362      exe 'sign unplace ' . (s:break_id + key)
363      unlet s:breakpoints[key]
364      break
365    endif
366  endfor
367endfunc
368
369" :Next, :Continue, etc - send a command to gdb
370func s:SendCommand(cmd)
371  call term_sendkeys(s:commbuf, a:cmd . "\r")
372endfunc
373
374func s:Run(args)
375  if a:args != ''
376    call s:SendCommand('-exec-arguments ' . a:args)
377  endif
378  call s:SendCommand('-exec-run')
379endfunc
380
381func s:SendEval(expr)
382  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
383  let s:evalexpr = a:expr
384endfunc
385
386" :Evaluate - evaluate what is under the cursor
387func s:Evaluate(range, arg)
388  if a:arg != ''
389    let expr = a:arg
390  elseif a:range == 2
391    let pos = getcurpos()
392    let reg = getreg('v', 1, 1)
393    let regt = getregtype('v')
394    normal! gv"vy
395    let expr = @v
396    call setpos('.', pos)
397    call setreg('v', reg, regt)
398  else
399    let expr = expand('<cexpr>')
400  endif
401  let s:ignoreEvalError = 0
402  call s:SendEval(expr)
403endfunc
404
405let s:ignoreEvalError = 0
406let s:evalFromBalloonExpr = 0
407
408" Handle the result of data-evaluate-expression
409func s:HandleEvaluate(msg)
410  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
411  let value = substitute(value, '\\"', '"', 'g')
412  if s:evalFromBalloonExpr
413    if s:evalFromBalloonExprResult == ''
414      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
415    else
416      let s:evalFromBalloonExprResult .= ' = ' . value
417    endif
418    call balloon_show(s:evalFromBalloonExprResult)
419  else
420    echomsg '"' . s:evalexpr . '": ' . value
421  endif
422
423  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
424    " Looks like a pointer, also display what it points to.
425    let s:ignoreEvalError = 1
426    call s:SendEval('*' . s:evalexpr)
427  else
428    let s:evalFromBalloonExpr = 0
429  endif
430endfunc
431
432" Show a balloon with information of the variable under the mouse pointer,
433" if there is any.
434func TermDebugBalloonExpr()
435  if v:beval_winid != s:startwin
436    return
437  endif
438  let s:evalFromBalloonExpr = 1
439  let s:evalFromBalloonExprResult = ''
440  let s:ignoreEvalError = 1
441  call s:SendEval(v:beval_text)
442  return ''
443endfunc
444
445" Handle an error.
446func s:HandleError(msg)
447  if s:ignoreEvalError
448    " Result of s:SendEval() failed, ignore.
449    let s:ignoreEvalError = 0
450    let s:evalFromBalloonExpr = 0
451    return
452  endif
453  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
454endfunc
455
456func s:GotoStartwinOrCreateIt()
457  if !win_gotoid(s:startwin)
458    new
459    let s:startwin = win_getid(winnr())
460    call s:InstallWinbar()
461  endif
462endfunc
463
464" Handle stopping and running message from gdb.
465" Will update the sign that shows the current position.
466func s:HandleCursor(msg)
467  let wid = win_getid(winnr())
468
469  if a:msg =~ '^\*stopped'
470    let s:stopped = 1
471  elseif a:msg =~ '^\*running'
472    let s:stopped = 0
473  endif
474
475  call s:GotoStartwinOrCreateIt()
476
477  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
478  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
479    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
480    if lnum =~ '^[0-9]*$'
481      if expand('%:p') != fnamemodify(fname, ':p')
482	if &modified
483	  " TODO: find existing window
484	  exe 'split ' . fnameescape(fname)
485	  let s:startwin = win_getid(winnr())
486	  call s:InstallWinbar()
487	else
488	  exe 'edit ' . fnameescape(fname)
489	endif
490      endif
491      exe lnum
492      exe 'sign unplace ' . s:pc_id
493      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
494      setlocal signcolumn=yes
495    endif
496  else
497    exe 'sign unplace ' . s:pc_id
498  endif
499
500  call win_gotoid(wid)
501endfunc
502
503" Handle setting a breakpoint
504" Will update the sign that shows the breakpoint
505func s:HandleNewBreakpoint(msg)
506  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
507  if nr == 0
508    return
509  endif
510
511  if has_key(s:breakpoints, nr)
512    let entry = s:breakpoints[nr]
513  else
514    let entry = {}
515    let s:breakpoints[nr] = entry
516  endif
517
518  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
519  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
520  let entry['fname'] = fname
521  let entry['lnum'] = lnum
522
523  if bufloaded(fname)
524    call s:PlaceSign(nr, entry)
525  endif
526endfunc
527
528func s:PlaceSign(nr, entry)
529  exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname']
530  let a:entry['placed'] = 1
531endfunc
532
533" Handle deleting a breakpoint
534" Will remove the sign that shows the breakpoint
535func s:HandleBreakpointDelete(msg)
536  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
537  if nr == 0
538    return
539  endif
540  if has_key(s:breakpoints, nr)
541    let entry = s:breakpoints[nr]
542    if has_key(entry, 'placed')
543      exe 'sign unplace ' . (s:break_id + nr)
544      unlet entry['placed']
545    endif
546    unlet s:breakpoints[nr]
547  endif
548endfunc
549
550" Handle a BufRead autocommand event: place any signs.
551func s:BufRead()
552  let fname = expand('<afile>:p')
553  for [nr, entry] in items(s:breakpoints)
554    if entry['fname'] == fname
555      call s:PlaceSign(nr, entry)
556    endif
557  endfor
558endfunc
559
560" Handle a BufUnloaded autocommand event: unplace any signs.
561func s:BufUnloaded()
562  let fname = expand('<afile>:p')
563  for [nr, entry] in items(s:breakpoints)
564    if entry['fname'] == fname
565      let entry['placed'] = 0
566    endif
567  endfor
568endfunc
569
570