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