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