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