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
58  " Create a hidden terminal window to communicate with gdb
59  let s:commbuf = term_start('NONE', {
60	\ 'term_name': 'gdb communication',
61	\ 'out_cb': function('s:CommOutput'),
62	\ 'hidden': 1,
63	\ })
64  if s:commbuf == 0
65    echoerr 'Failed to open the communication terminal window'
66    exe 'bwipe! ' . s:ptybuf
67    return
68  endif
69  let commpty = job_info(term_getjob(s:commbuf))['tty_out']
70
71  " Open a terminal window to run the debugger.
72  let cmd = [g:termdebugger, '-tty', pty, a:cmd]
73  echomsg 'executing "' . join(cmd) . '"'
74  let gdbbuf = term_start(cmd, {
75	\ 'exit_cb': function('s:EndDebug'),
76	\ 'term_finish': 'close',
77	\ })
78  if gdbbuf == 0
79    echoerr 'Failed to open the gdb terminal window'
80    exe 'bwipe! ' . s:ptybuf
81    exe 'bwipe! ' . s:commbuf
82    return
83  endif
84
85  " Connect gdb to the communication pty, using the GDB/MI interface
86  call term_sendkeys(gdbbuf, 'new-ui mi ' . commpty . "\r")
87
88  " Install debugger commands.
89  call s:InstallCommands()
90
91  let s:breakpoints = {}
92endfunc
93
94func s:EndDebug(job, status)
95  exe 'bwipe! ' . s:ptybuf
96  exe 'bwipe! ' . s:commbuf
97
98  let curwinid = win_getid(winnr())
99
100  call win_gotoid(s:startwin)
101  let &signcolumn = s:startsigncolumn
102  call s:DeleteCommands()
103
104  call win_gotoid(curwinid)
105endfunc
106
107" Handle a message received from gdb on the GDB/MI interface.
108func s:CommOutput(chan, msg)
109  let msgs = split(a:msg, "\r")
110
111  for msg in msgs
112    " remove prefixed NL
113    if msg[0] == "\n"
114      let msg = msg[1:]
115    endif
116    if msg != ''
117      if msg =~ '^\*\(stopped\|running\)'
118	call s:HandleCursor(msg)
119      elseif msg =~ '^\^done,bkpt='
120	call s:HandleNewBreakpoint(msg)
121      elseif msg =~ '^=breakpoint-deleted,'
122	call s:HandleBreakpointDelete(msg)
123      endif
124    endif
125  endfor
126endfunc
127
128" Install commands in the current window to control the debugger.
129func s:InstallCommands()
130  command Break call s:SetBreakpoint()
131  command Delete call s:DeleteBreakpoint()
132  command Step call s:SendCommand('-exec-step')
133  command NNext call s:SendCommand('-exec-next')
134  command Finish call s:SendCommand('-exec-finish')
135  command Continue call s:SendCommand('-exec-continue')
136endfunc
137
138" Delete installed debugger commands in the current window.
139func s:DeleteCommands()
140  delcommand Break
141  delcommand Delete
142  delcommand Step
143  delcommand NNext
144  delcommand Finish
145  delcommand Continue
146endfunc
147
148" :Break - Set a breakpoint at the cursor position.
149func s:SetBreakpoint()
150  call term_sendkeys(s:commbuf, '-break-insert --source '
151	\ . fnameescape(expand('%:p')) . ' --line ' . line('.') . "\r")
152endfunc
153
154" :Delete - Delete a breakpoint at the cursor position.
155func s:DeleteBreakpoint()
156  let fname = fnameescape(expand('%:p'))
157  let lnum = line('.')
158  for [key, val] in items(s:breakpoints)
159    if val['fname'] == fname && val['lnum'] == lnum
160      call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r")
161      " Assume this always wors, the reply is simply "^done".
162      exe 'sign unplace ' . (s:break_id + key)
163      unlet s:breakpoints[key]
164      break
165    endif
166  endfor
167endfunc
168
169" :Next, :Continue, etc - send a command to gdb
170func s:SendCommand(cmd)
171  call term_sendkeys(s:commbuf, a:cmd . "\r")
172endfunc
173
174" Handle stopping and running message from gdb.
175" Will update the sign that shows the current position.
176func s:HandleCursor(msg)
177  let wid = win_getid(winnr())
178
179  if win_gotoid(s:startwin)
180    if a:msg =~ '^\*stopped'
181      let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
182      let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
183      if lnum =~ '^[0-9]*$'
184	if expand('%:h') != fname
185	  if &modified
186	    " TODO: find existing window
187	    exe 'split ' . fnameescape(fname)
188	    let s:startwin = win_getid(winnr())
189	  else
190	    exe 'edit ' . fnameescape(fname)
191	  endif
192	endif
193	exe lnum
194	exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fnameescape(fname)
195	setlocal signcolumn=yes
196      endif
197    else
198      exe 'sign unplace ' . s:pc_id
199    endif
200
201    call win_gotoid(wid)
202  endif
203endfunc
204
205" Handle setting a breakpoint
206" Will update the sign that shows the breakpoint
207func s:HandleNewBreakpoint(msg)
208  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
209  if nr == 0
210    return
211  endif
212
213  if has_key(s:breakpoints, nr)
214    let entry = s:breakpoints[nr]
215  else
216    let entry = {}
217    let s:breakpoints[nr] = entry
218  endif
219
220  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
221  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
222
223  exe 'sign place ' . (s:break_id + nr) . ' line=' . lnum . ' name=debugBreakpoint file=' . fnameescape(fname)
224
225  let entry['fname'] = fname
226  let entry['lnum'] = lnum
227endfunc
228
229" Handle deleting a breakpoint
230" Will remove the sign that shows the breakpoint
231func s:HandleBreakpointDelete(msg)
232  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
233  if nr == 0
234    return
235  endif
236  exe 'sign unplace ' . (s:break_id + nr)
237  unlet s:breakpoints[nr]
238endfunc
239