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  call s:SendEval(expr)
329endfunc
330
331let s:evalFromBalloonExpr = 0
332
333" Handle the result of data-evaluate-expression
334func s:HandleEvaluate(msg)
335  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
336  let value = substitute(value, '\\"', '"', 'g')
337  if s:evalFromBalloonExpr
338    if s:evalFromBalloonExprResult == ''
339      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
340    else
341      let s:evalFromBalloonExprResult .= ' = ' . value
342    endif
343    call balloon_show(s:evalFromBalloonExprResult)
344  else
345    echomsg '"' . s:evalexpr . '": ' . value
346  endif
347
348  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
349    " Looks like a pointer, also display what it points to.
350    call s:SendEval('*' . s:evalexpr)
351  else
352    let s:evalFromBalloonExpr = 0
353  endif
354endfunc
355
356" Show a balloon with information of the variable under the mouse pointer,
357" if there is any.
358func TermDebugBalloonExpr()
359  if v:beval_winid != s:startwin
360    return
361  endif
362  call s:SendEval(v:beval_text)
363  let s:evalFromBalloonExpr = 1
364  let s:evalFromBalloonExprResult = ''
365  return ''
366endfunc
367
368" Handle an error.
369func s:HandleError(msg)
370  if a:msg =~ 'No symbol .* in current context'
371	\ || a:msg =~ 'Cannot access memory at address '
372	\ || a:msg =~ 'Attempt to use a type name as an expression'
373	\ || a:msg =~ 'A syntax error in expression,'
374    " Result of s:SendEval() failed, ignore.
375    return
376  endif
377  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
378endfunc
379
380" Handle stopping and running message from gdb.
381" Will update the sign that shows the current position.
382func s:HandleCursor(msg)
383  let wid = win_getid(winnr())
384
385  if a:msg =~ '^\*stopped'
386    let s:stopped = 1
387  elseif a:msg =~ '^\*running'
388    let s:stopped = 0
389  endif
390
391  if win_gotoid(s:startwin)
392    let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
393    if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
394      let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
395      if lnum =~ '^[0-9]*$'
396	if expand('%:p') != fnamemodify(fname, ':p')
397	  if &modified
398	    " TODO: find existing window
399	    exe 'split ' . fnameescape(fname)
400	    let s:startwin = win_getid(winnr())
401	  else
402	    exe 'edit ' . fnameescape(fname)
403	  endif
404	endif
405	exe lnum
406	exe 'sign unplace ' . s:pc_id
407	exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
408	setlocal signcolumn=yes
409      endif
410    else
411      exe 'sign unplace ' . s:pc_id
412    endif
413
414    call win_gotoid(wid)
415  endif
416endfunc
417
418" Handle setting a breakpoint
419" Will update the sign that shows the breakpoint
420func s:HandleNewBreakpoint(msg)
421  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
422  if nr == 0
423    return
424  endif
425
426  if has_key(s:breakpoints, nr)
427    let entry = s:breakpoints[nr]
428  else
429    let entry = {}
430    let s:breakpoints[nr] = entry
431  endif
432
433  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
434  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
435  let entry['fname'] = fname
436  let entry['lnum'] = lnum
437
438  if bufloaded(fname)
439    call s:PlaceSign(nr, entry)
440  endif
441endfunc
442
443func s:PlaceSign(nr, entry)
444  exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname']
445  let a:entry['placed'] = 1
446endfunc
447
448" Handle deleting a breakpoint
449" Will remove the sign that shows the breakpoint
450func s:HandleBreakpointDelete(msg)
451  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
452  if nr == 0
453    return
454  endif
455  if has_key(s:breakpoints, nr)
456    let entry = s:breakpoints[nr]
457    if has_key(entry, 'placed')
458      exe 'sign unplace ' . (s:break_id + nr)
459      unlet entry['placed']
460    endif
461    unlet s:breakpoints[nr]
462  endif
463endfunc
464
465" Handle a BufRead autocommand event: place any signs.
466func s:BufRead()
467  let fname = expand('<afile>:p')
468  for [nr, entry] in items(s:breakpoints)
469    if entry['fname'] == fname
470      call s:PlaceSign(nr, entry)
471    endif
472  endfor
473endfunc
474
475" Handle a BufUnloaded autocommand event: unplace any signs.
476func s:BufUnloaded()
477  let fname = expand('<afile>:p')
478  for [nr, entry] in items(s:breakpoints)
479    if entry['fname'] == fname
480      let entry['placed'] = 0
481    endif
482  endfor
483endfunc
484
485