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