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