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