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