1sign define GdbBreakpoint text=●
2sign define GdbCurrentLine text=⇒
3
4
5let s:gdb_port = 7778
6let s:run_gdb = "gdb -q -f build/bin/nvim"
7let s:breakpoints = {}
8let s:max_breakpoint_sign_id = 0
9
10
11let s:GdbServer = {}
12
13
14function s:GdbServer.new(gdb)
15  let this = copy(self)
16  let this._gdb = a:gdb
17  return this
18endfunction
19
20
21function s:GdbServer.on_exit()
22  let self._gdb._server_exited = 1
23endfunction
24
25
26let s:GdbPaused = vimexpect#State([
27      \ ['Continuing.', 'continue'],
28      \ ['\v[\o32]{2}([^:]+):(\d+):\d+', 'jump'],
29      \ ['Remote communication error.  Target disconnected.:', 'retry'],
30      \ ])
31
32
33function s:GdbPaused.continue(...)
34  call self._parser.switch(s:GdbRunning)
35  call self.update_current_line_sign(0)
36endfunction
37
38
39function s:GdbPaused.jump(file, line, ...)
40  if tabpagenr() != self._tab
41    " Don't jump if we are not in the debugger tab
42    return
43  endif
44  let window = winnr()
45  exe self._jump_window 'wincmd w'
46  let self._current_buf = bufnr('%')
47  let target_buf = bufnr(a:file, 1)
48  if bufnr('%') != target_buf
49    exe 'buffer ' target_buf
50    let self._current_buf = target_buf
51  endif
52  exe ':' a:line
53  let self._current_line = a:line
54  exe window 'wincmd w'
55  call self.update_current_line_sign(1)
56endfunction
57
58
59function s:GdbPaused.retry(...)
60  if self._server_exited
61    return
62  endif
63  sleep 1
64  call self.attach()
65  call self.send('continue')
66endfunction
67
68
69let s:GdbRunning = vimexpect#State([
70      \ ['\v^Breakpoint \d+', 'pause'],
71      \ ['\v\[Inferior\ +.{-}\ +exited\ +normally', 'disconnected'],
72      \ ['(gdb)', 'pause'],
73      \ ])
74
75
76function s:GdbRunning.pause(...)
77  call self._parser.switch(s:GdbPaused)
78  if !self._initialized
79    call self.send('set confirm off')
80    call self.send('set pagination off')
81    if !empty(self._server_addr)
82      call self.send('set remotetimeout 50')
83      call self.attach()
84      call s:RefreshBreakpoints()
85      call self.send('c')
86    endif
87    let self._initialized = 1
88  endif
89endfunction
90
91
92function s:GdbRunning.disconnected(...)
93  if !self._server_exited && self._reconnect
94    " Refresh to force a delete of all watchpoints
95    call s:RefreshBreakpoints()
96    sleep 1
97    call self.attach()
98    call self.send('continue')
99  endif
100endfunction
101
102
103let s:Gdb = {}
104
105
106function s:Gdb.kill()
107  tunmap <f8>
108  tunmap <f10>
109  tunmap <f11>
110  tunmap <f12>
111  call self.update_current_line_sign(0)
112  exe 'bd! '.self._client_buf
113  if self._server_buf != -1
114    exe 'bd! '.self._server_buf
115  endif
116  exe 'tabnext '.self._tab
117  tabclose
118  unlet g:gdb
119endfunction
120
121
122function! s:Gdb.send(data)
123  call jobsend(self._client_id, a:data."\<cr>")
124endfunction
125
126
127function! s:Gdb.attach()
128  call self.send(printf('target remote %s', self._server_addr))
129endfunction
130
131
132function! s:Gdb.update_current_line_sign(add)
133  " to avoid flicker when removing/adding the sign column(due to the change in
134  " line width), we switch ids for the line sign and only remove the old line
135  " sign after marking the new one
136  let old_line_sign_id = get(self, '_line_sign_id', 4999)
137  let self._line_sign_id = old_line_sign_id == 4999 ? 4998 : 4999
138  if a:add && self._current_line != -1 && self._current_buf != -1
139    exe 'sign place '.self._line_sign_id.' name=GdbCurrentLine line='
140          \.self._current_line.' buffer='.self._current_buf
141  endif
142  exe 'sign unplace '.old_line_sign_id
143endfunction
144
145
146function! s:Spawn(server_cmd, client_cmd, server_addr, reconnect)
147  if exists('g:gdb')
148    throw 'Gdb already running'
149  endif
150  let gdb = vimexpect#Parser(s:GdbRunning, copy(s:Gdb))
151  " gdbserver port
152  let gdb._server_addr = a:server_addr
153  let gdb._reconnect = a:reconnect
154  let gdb._initialized = 0
155  " window number that will be displaying the current file
156  let gdb._jump_window = 1
157  let gdb._current_buf = -1
158  let gdb._current_line = -1
159  let gdb._has_breakpoints = 0
160  let gdb._server_exited = 0
161  " Create new tab for the debugging view
162  tabnew
163  let gdb._tab = tabpagenr()
164  " create horizontal split to display the current file and maybe gdbserver
165  sp
166  let gdb._server_buf = -1
167  if type(a:server_cmd) == type('')
168    " spawn gdbserver in a vertical split
169    let server = s:GdbServer.new(gdb)
170    vsp | enew | let gdb._server_id = termopen(a:server_cmd, server)
171    let gdb._jump_window = 2
172    let gdb._server_buf = bufnr('%')
173  endif
174  " go to the bottom window and spawn gdb client
175  wincmd j
176  enew | let gdb._client_id = termopen(a:client_cmd, gdb)
177  let gdb._client_buf = bufnr('%')
178  tnoremap <silent> <f8> <c-\><c-n>:GdbContinue<cr>i
179  tnoremap <silent> <f10> <c-\><c-n>:GdbNext<cr>i
180  tnoremap <silent> <f11> <c-\><c-n>:GdbStep<cr>i
181  tnoremap <silent> <f12> <c-\><c-n>:GdbFinish<cr>i
182  " go to the window that displays the current file
183  exe gdb._jump_window 'wincmd w'
184  let g:gdb = gdb
185endfunction
186
187
188function! s:Test(bang, filter)
189  let cmd = "GDB=1 make test"
190  if a:bang == '!'
191    let server_addr = '| vgdb'
192    let cmd = printf('VALGRIND=1 %s', cmd)
193  else
194    let server_addr = printf('localhost:%d', s:gdb_port)
195    let cmd = printf('GDBSERVER_PORT=%d %s', s:gdb_port, cmd)
196  endif
197  if a:filter != ''
198    let cmd = printf('TEST_SCREEN_TIMEOUT=1000000 TEST_FILTER="%s" %s', a:filter, cmd)
199  endif
200  call s:Spawn(cmd, s:run_gdb, server_addr, 1)
201endfunction
202
203
204function! s:ToggleBreak()
205  let file_name = bufname('%')
206  let file_breakpoints = get(s:breakpoints, file_name, {})
207  let linenr = line('.')
208  if has_key(file_breakpoints, linenr)
209    call remove(file_breakpoints, linenr)
210  else
211    let file_breakpoints[linenr] = 1
212  endif
213  let s:breakpoints[file_name] = file_breakpoints
214  call s:RefreshBreakpointSigns()
215  call s:RefreshBreakpoints()
216endfunction
217
218
219function! s:ClearBreak()
220  let s:breakpoints = {}
221  call s:RefreshBreakpointSigns()
222  call s:RefreshBreakpoints()
223endfunction
224
225
226function! s:RefreshBreakpointSigns()
227  let buf = bufnr('%')
228  let i = 5000
229  while i <= s:max_breakpoint_sign_id
230    exe 'sign unplace '.i
231    let i += 1
232  endwhile
233  let s:max_breakpoint_sign_id = 0
234  let id = 5000
235  for linenr in keys(get(s:breakpoints, bufname('%'), {}))
236    exe 'sign place '.id.' name=GdbBreakpoint line='.linenr.' buffer='.buf
237    let s:max_breakpoint_sign_id = id
238    let id += 1
239  endfor
240endfunction
241
242
243function! s:RefreshBreakpoints()
244  if !exists('g:gdb')
245    return
246  endif
247  if g:gdb._parser.state() == s:GdbRunning
248    " pause first
249    call jobsend(g:gdb._client_id, "\<c-c>")
250  endif
251  if g:gdb._has_breakpoints
252    call g:gdb.send('delete')
253  endif
254  let g:gdb._has_breakpoints = 0
255  for [file, breakpoints] in items(s:breakpoints)
256    for linenr in keys(breakpoints)
257      let g:gdb._has_breakpoints = 1
258      call g:gdb.send('break '.file.':'.linenr)
259    endfor
260  endfor
261endfunction
262
263
264function! s:GetExpression(...) range
265  let [lnum1, col1] = getpos("'<")[1:2]
266  let [lnum2, col2] = getpos("'>")[1:2]
267  let lines = getline(lnum1, lnum2)
268  let lines[-1] = lines[-1][:col2 - 1]
269  let lines[0] = lines[0][col1 - 1:]
270  return join(lines, "\n")
271endfunction
272
273
274function! s:Send(data)
275  if !exists('g:gdb')
276    throw 'Gdb is not running'
277  endif
278  call g:gdb.send(a:data)
279endfunction
280
281
282function! s:Eval(expr)
283  call s:Send(printf('print %s', a:expr))
284endfunction
285
286
287function! s:Watch(expr)
288  let expr = a:expr
289  if expr[0] != '&'
290    let expr = '&' . expr
291  endif
292
293  call s:Eval(expr)
294  call s:Send('watch *$')
295endfunction
296
297
298function! s:Interrupt()
299  if !exists('g:gdb')
300    throw 'Gdb is not running'
301  endif
302  call jobsend(g:gdb._client_id, "\<c-c>info line\<cr>")
303endfunction
304
305
306function! s:Kill()
307  if !exists('g:gdb')
308    throw 'Gdb is not running'
309  endif
310  call g:gdb.kill()
311endfunction
312
313
314command! GdbDebugNvim call s:Spawn(printf('make && gdbserver localhost:%d build/bin/nvim', s:gdb_port), s:run_gdb, printf('localhost:%d', s:gdb_port), 0)
315command! -nargs=1 GdbDebugServer call s:Spawn(0, s:run_gdb, 'localhost:'.<q-args>, 0)
316command! -bang -nargs=? GdbDebugTest call s:Test(<q-bang>, <q-args>)
317command! -nargs=1 -complete=file GdbInspectCore call s:Spawn(0, printf('gdb -q -f -c %s build/bin/nvim', <q-args>), 0, 0)
318command! GdbDebugStop call s:Kill()
319command! GdbToggleBreakpoint call s:ToggleBreak()
320command! GdbClearBreakpoints call s:ClearBreak()
321command! GdbContinue call s:Send("c")
322command! GdbNext call s:Send("n")
323command! GdbStep call s:Send("s")
324command! GdbFinish call s:Send("finish")
325command! GdbFrameUp call s:Send("up")
326command! GdbFrameDown call s:Send("down")
327command! GdbInterrupt call s:Interrupt()
328command! GdbEvalWord call s:Eval(expand('<cword>'))
329command! -range GdbEvalRange call s:Eval(s:GetExpression(<f-args>))
330command! GdbWatchWord call s:Watch(expand('<cword>')
331command! -range GdbWatchRange call s:Watch(s:GetExpression(<f-args>))
332
333
334nnoremap <silent> <f8> :GdbContinue<cr>
335nnoremap <silent> <f10> :GdbNext<cr>
336nnoremap <silent> <f11> :GdbStep<cr>
337nnoremap <silent> <f12> :GdbFinish<cr>
338nnoremap <silent> <c-b> :GdbToggleBreakpoint<cr>
339nnoremap <silent> <m-pageup> :GdbFrameUp<cr>
340nnoremap <silent> <m-pagedown> :GdbFrameDown<cr>
341nnoremap <silent> <f9> :GdbEvalWord<cr>
342vnoremap <silent> <f9> :GdbEvalRange<cr>
343nnoremap <silent> <m-f9> :GdbWatchWord<cr>
344vnoremap <silent> <m-f9> :GdbWatchRange<cr>
345