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