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