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" The command that starts debugging, e.g. ":Termdebug vim".
19" To end type "quit" in the gdb window.
20command -nargs=* -complete=file Termdebug call s:StartDebug(<q-args>)
21
22" Name of the gdb command, defaults to "gdb".
23if !exists('termdebugger')
24  let termdebugger = 'gdb'
25endif
26
27let s:pc_id = 12
28let s:break_id = 13
29
30if &background == 'light'
31  hi default debugPC term=reverse ctermbg=lightblue guibg=lightblue
32else
33  hi default debugPC term=reverse ctermbg=darkblue guibg=darkblue
34endif
35hi default debugBreakpoint term=reverse ctermbg=red guibg=red
36
37func s:StartDebug(cmd)
38  let s:startwin = win_getid(winnr())
39  let s:startsigncolumn = &signcolumn
40
41  if exists('g:termdebug_wide') && &columns < g:termdebug_wide
42    let s:save_columns = &columns
43    let &columns = g:termdebug_wide
44    let vertical = 1
45  else
46    let s:save_columns = 0
47    let vertical = 0
48  endif
49
50  " Open a terminal window without a job, to run the debugged program
51  let s:ptybuf = term_start('NONE', {
52	\ 'term_name': 'gdb program',
53	\ 'vertical': vertical,
54	\ })
55  if s:ptybuf == 0
56    echoerr 'Failed to open the program terminal window'
57    return
58  endif
59  let pty = job_info(term_getjob(s:ptybuf))['tty_out']
60  let s:ptywin = win_getid(winnr())
61
62  " Create a hidden terminal window to communicate with gdb
63  let s:commbuf = term_start('NONE', {
64	\ 'term_name': 'gdb communication',
65	\ 'out_cb': function('s:CommOutput'),
66	\ 'hidden': 1,
67	\ })
68  if s:commbuf == 0
69    echoerr 'Failed to open the communication terminal window'
70    exe 'bwipe! ' . s:ptybuf
71    return
72  endif
73  let commpty = job_info(term_getjob(s:commbuf))['tty_out']
74
75  " Open a terminal window to run the debugger.
76  let cmd = [g:termdebugger, '-tty', pty, a:cmd]
77  echomsg 'executing "' . join(cmd) . '"'
78  let gdbbuf = term_start(cmd, {
79	\ 'exit_cb': function('s:EndDebug'),
80	\ 'term_finish': 'close',
81	\ })
82  if gdbbuf == 0
83    echoerr 'Failed to open the gdb terminal window'
84    exe 'bwipe! ' . s:ptybuf
85    exe 'bwipe! ' . s:commbuf
86    return
87  endif
88  let s:gdbwin = win_getid(winnr())
89
90  " Connect gdb to the communication pty, using the GDB/MI interface
91  call term_sendkeys(gdbbuf, 'new-ui mi ' . commpty . "\r")
92
93  " Sign used to highlight the line where the program has stopped.
94  " There can be only one.
95  sign define debugPC linehl=debugPC
96
97  " Sign used to indicate a breakpoint.
98  " Can be used multiple times.
99  sign define debugBreakpoint text=>> texthl=debugBreakpoint
100
101  " Install debugger commands in the text window.
102  call win_gotoid(s:startwin)
103  call s:InstallCommands()
104  call win_gotoid(s:gdbwin)
105
106  let s:breakpoints = {}
107
108  augroup TermDebug
109    au BufRead * call s:BufRead()
110    au BufUnload * call s:BufUnloaded()
111  augroup END
112endfunc
113
114func s:EndDebug(job, status)
115  exe 'bwipe! ' . s:ptybuf
116  exe 'bwipe! ' . s:commbuf
117
118  let curwinid = win_getid(winnr())
119
120  call win_gotoid(s:startwin)
121  let &signcolumn = s:startsigncolumn
122  call s:DeleteCommands()
123
124  call win_gotoid(curwinid)
125  if s:save_columns > 0
126    let &columns = s:save_columns
127  endif
128
129  au! TermDebug
130endfunc
131
132" Handle a message received from gdb on the GDB/MI interface.
133func s:CommOutput(chan, msg)
134  let msgs = split(a:msg, "\r")
135
136  for msg in msgs
137    " remove prefixed NL
138    if msg[0] == "\n"
139      let msg = msg[1:]
140    endif
141    if msg != ''
142      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
143	call s:HandleCursor(msg)
144      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
145	call s:HandleNewBreakpoint(msg)
146      elseif msg =~ '^=breakpoint-deleted,'
147	call s:HandleBreakpointDelete(msg)
148      elseif msg =~ '^\^done,value='
149	call s:HandleEvaluate(msg)
150      elseif msg =~ '^\^error,msg='
151	call s:HandleError(msg)
152      endif
153    endif
154  endfor
155endfunc
156
157" Install commands in the current window to control the debugger.
158func s:InstallCommands()
159  command Break call s:SetBreakpoint()
160  command Delete call s:DeleteBreakpoint()
161  command Step call s:SendCommand('-exec-step')
162  command Over call s:SendCommand('-exec-next')
163  command Finish call s:SendCommand('-exec-finish')
164  command Continue call s:SendCommand('-exec-continue')
165  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
166  command Gdb call win_gotoid(s:gdbwin)
167  command Program call win_gotoid(s:ptywin)
168
169  " TODO: can the K mapping be restored?
170  nnoremap K :Evaluate<CR>
171
172  if has('menu')
173    amenu WinBar.Step :Step<CR>
174    amenu WinBar.Next :Over<CR>
175    amenu WinBar.Finish :Finish<CR>
176    amenu WinBar.Cont :Continue<CR>
177    amenu WinBar.Eval :Evaluate<CR>
178  endif
179endfunc
180
181" Delete installed debugger commands in the current window.
182func s:DeleteCommands()
183  delcommand Break
184  delcommand Delete
185  delcommand Step
186  delcommand Over
187  delcommand Finish
188  delcommand Continue
189  delcommand Evaluate
190  delcommand Gdb
191  delcommand Program
192
193  nunmap K
194
195  if has('menu')
196    aunmenu WinBar.Step
197    aunmenu WinBar.Next
198    aunmenu WinBar.Finish
199    aunmenu WinBar.Cont
200    aunmenu WinBar.Eval
201  endif
202
203  exe 'sign unplace ' . s:pc_id
204  for key in keys(s:breakpoints)
205    exe 'sign unplace ' . (s:break_id + key)
206  endfor
207  sign undefine debugPC
208  sign undefine debugBreakpoint
209  unlet s:breakpoints
210endfunc
211
212" :Break - Set a breakpoint at the cursor position.
213func s:SetBreakpoint()
214  call term_sendkeys(s:commbuf, '-break-insert --source '
215	\ . fnameescape(expand('%:p')) . ' --line ' . line('.') . "\r")
216endfunc
217
218" :Delete - Delete a breakpoint at the cursor position.
219func s:DeleteBreakpoint()
220  let fname = fnameescape(expand('%:p'))
221  let lnum = line('.')
222  for [key, val] in items(s:breakpoints)
223    if val['fname'] == fname && val['lnum'] == lnum
224      call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r")
225      " Assume this always wors, the reply is simply "^done".
226      exe 'sign unplace ' . (s:break_id + key)
227      unlet s:breakpoints[key]
228      break
229    endif
230  endfor
231endfunc
232
233" :Next, :Continue, etc - send a command to gdb
234func s:SendCommand(cmd)
235  call term_sendkeys(s:commbuf, a:cmd . "\r")
236endfunc
237
238" :Evaluate - evaluate what is under the cursor
239func s:Evaluate(range, arg)
240  if a:arg != ''
241    let expr = a:arg
242  elseif a:range == 2
243    let pos = getcurpos()
244    let reg = getreg('v', 1, 1)
245    let regt = getregtype('v')
246    normal! gv"vy
247    let expr = @v
248    call setpos('.', pos)
249    call setreg('v', reg, regt)
250  else
251    let expr = expand('<cexpr>')
252  endif
253  call term_sendkeys(s:commbuf, '-data-evaluate-expression "' . expr . "\"\r")
254  let s:evalexpr = expr
255endfunc
256
257" Handle the result of data-evaluate-expression
258func s:HandleEvaluate(msg)
259  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
260  let value = substitute(value, '\\"', '"', 'g')
261  echomsg '"' . s:evalexpr . '": ' . value
262
263  if s:evalexpr[0] != '*' && value =~ '^0x' && value !~ '"$'
264    " Looks like a pointer, also display what it points to.
265    let s:evalexpr = '*' . s:evalexpr
266    call term_sendkeys(s:commbuf, '-data-evaluate-expression "' . s:evalexpr . "\"\r")
267  endif
268endfunc
269
270" Handle an error.
271func s:HandleError(msg)
272  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
273endfunc
274
275" Handle stopping and running message from gdb.
276" Will update the sign that shows the current position.
277func s:HandleCursor(msg)
278  let wid = win_getid(winnr())
279
280  if win_gotoid(s:startwin)
281    let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
282    if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
283      let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
284      if lnum =~ '^[0-9]*$'
285	if expand('%:p') != fnamemodify(fname, ':p')
286	  if &modified
287	    " TODO: find existing window
288	    exe 'split ' . fnameescape(fname)
289	    let s:startwin = win_getid(winnr())
290	  else
291	    exe 'edit ' . fnameescape(fname)
292	  endif
293	endif
294	exe lnum
295	exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
296	setlocal signcolumn=yes
297      endif
298    else
299      exe 'sign unplace ' . s:pc_id
300    endif
301
302    call win_gotoid(wid)
303  endif
304endfunc
305
306" Handle setting a breakpoint
307" Will update the sign that shows the breakpoint
308func s:HandleNewBreakpoint(msg)
309  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
310  if nr == 0
311    return
312  endif
313
314  if has_key(s:breakpoints, nr)
315    let entry = s:breakpoints[nr]
316  else
317    let entry = {}
318    let s:breakpoints[nr] = entry
319  endif
320
321  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
322  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
323  let entry['fname'] = fname
324  let entry['lnum'] = lnum
325
326  if bufloaded(fname)
327    call s:PlaceSign(nr, entry)
328  endif
329endfunc
330
331func s:PlaceSign(nr, entry)
332  exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint file=' . a:entry['fname']
333  let a:entry['placed'] = 1
334endfunc
335
336" Handle deleting a breakpoint
337" Will remove the sign that shows the breakpoint
338func s:HandleBreakpointDelete(msg)
339  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
340  if nr == 0
341    return
342  endif
343  if has_key(s:breakpoints, nr)
344    let entry = s:breakpoints[nr]
345    if has_key(entry, 'placed')
346      exe 'sign unplace ' . (s:break_id + nr)
347      unlet entry['placed']
348    endif
349    unlet s:breakpoints[nr]
350  endif
351endfunc
352
353" Handle a BufRead autocommand event: place any signs.
354func s:BufRead()
355  let fname = expand('<afile>:p')
356  for [nr, entry] in items(s:breakpoints)
357    if entry['fname'] == fname
358      call s:PlaceSign(nr, entry)
359    endif
360  endfor
361endfunc
362
363" Handle a BufUnloaded autocommand event: unplace any signs.
364func s:BufUnloaded()
365  let fname = expand('<afile>:p')
366  for [nr, entry] in items(s:breakpoints)
367    if entry['fname'] == fname
368      let entry['placed'] = 0
369    endif
370  endfor
371endfunc
372
373