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