aboutsummaryrefslogtreecommitdiff
path: root/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
blob: 46bd42112de41ab883ee87dd12d3f13b0635057f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
" Debugger plugin using gdb.
"
" WORK IN PROGRESS - much doesn't work yet
"
" Open two visible terminal windows:
" 1. run a pty, as with ":term NONE"
" 2. run gdb, passing the pty
" The current window is used to view source code and follows gdb.
"
" A third terminal window is hidden, it is used for communication with gdb.
"
" The communication with gdb uses GDB/MI.  See:
" https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html
"
" Author: Bram Moolenaar
" Copyright: Vim license applies, see ":help license"

" In case this gets loaded twice.
if exists(':Termdebug')
  finish
endif

" The command that starts debugging, e.g. ":Termdebug vim".
" To end type "quit" in the gdb window.
command -nargs=* -complete=file Termdebug call s:StartDebug(<q-args>)

" Name of the gdb command, defaults to "gdb".
if !exists('termdebugger')
  let termdebugger = 'gdb'
endif

" Sign used to highlight the line where the program has stopped.
" There can be only one.
sign define debugPC linehl=debugPC
let s:pc_id = 12
let s:break_id = 13

" Sign used to indicate a breakpoint.
" Can be used multiple times.
sign define debugBreakpoint text=>> texthl=debugBreakpoint

if &background == 'light'
  hi default debugPC term=reverse ctermbg=lightblue guibg=lightblue
else
  hi default debugPC term=reverse ctermbg=darkblue guibg=darkblue
endif
hi default debugBreakpoint term=reverse ctermbg=red guibg=red

func s:StartDebug(cmd)
  let s:startwin = win_getid(winnr())
  let s:startsigncolumn = &signcolumn

  " Open a terminal window without a job, to run the debugged program
  let s:ptybuf = term_start('NONE', {
	\ 'term_name': 'gdb program',
	\ })
  if s:ptybuf == 0
    echoerr 'Failed to open the program terminal window'
    return
  endif
  let pty = job_info(term_getjob(s:ptybuf))['tty_out']

  " Create a hidden terminal window to communicate with gdb
  let s:commbuf = term_start('NONE', {
	\ 'term_name': 'gdb communication',
	\ 'out_cb': function('s:CommOutput'),
	\ 'hidden': 1,
	\ })
  if s:commbuf == 0
    echoerr 'Failed to open the communication terminal window'
    exe 'bwipe! ' . s:ptybuf
    return
  endif
  let commpty = job_info(term_getjob(s:commbuf))['tty_out']

  " Open a terminal window to run the debugger.
  let cmd = [g:termdebugger, '-tty', pty, a:cmd]
  echomsg 'executing "' . join(cmd) . '"'
  let gdbbuf = term_start(cmd, {
	\ 'exit_cb': function('s:EndDebug'),
	\ 'term_finish': 'close',
	\ })
  if gdbbuf == 0
    echoerr 'Failed to open the gdb terminal window'
    exe 'bwipe! ' . s:ptybuf
    exe 'bwipe! ' . s:commbuf
    return
  endif

  " Connect gdb to the communication pty, using the GDB/MI interface
  call term_sendkeys(gdbbuf, 'new-ui mi ' . commpty . "\r")

  " Install debugger commands.
  call s:InstallCommands()

  let s:breakpoints = {}
endfunc

func s:EndDebug(job, status)
  exe 'bwipe! ' . s:ptybuf
  exe 'bwipe! ' . s:commbuf

  let curwinid = win_getid(winnr())

  call win_gotoid(s:startwin)
  let &signcolumn = s:startsigncolumn
  call s:DeleteCommands()

  call win_gotoid(curwinid)
endfunc

" Handle a message received from gdb on the GDB/MI interface.
func s:CommOutput(chan, msg)
  let msgs = split(a:msg, "\r")

  for msg in msgs
    " remove prefixed NL
    if msg[0] == "\n"
      let msg = msg[1:]
    endif
    if msg != ''
      if msg =~ '^\*\(stopped\|running\)'
	call s:HandleCursor(msg)
      elseif msg =~ '^\^done,bkpt='
	call s:HandleNewBreakpoint(msg)
      elseif msg =~ '^=breakpoint-deleted,'
	call s:HandleBreakpointDelete(msg)
      endif
    endif
  endfor
endfunc

" Install commands in the current window to control the debugger.
func s:InstallCommands()
  command Break call s:SetBreakpoint()
  command Delete call s:DeleteBreakpoint()
  command Step call s:SendCommand('-exec-step')
  command NNext call s:SendCommand('-exec-next')
  command Finish call s:SendCommand('-exec-finish')
  command Continue call s:SendCommand('-exec-continue')
endfunc

" Delete installed debugger commands in the current window.
func s:DeleteCommands()
  delcommand Break
  delcommand Delete
  delcommand Step
  delcommand NNext
  delcommand Finish
  delcommand Continue
endfunc

" :Break - Set a breakpoint at the cursor position.
func s:SetBreakpoint()
  call term_sendkeys(s:commbuf, '-break-insert --source '
	\ . fnameescape(expand('%:p')) . ' --line ' . line('.') . "\r")
endfunc

" :Delete - Delete a breakpoint at the cursor position.
func s:DeleteBreakpoint()
  let fname = fnameescape(expand('%:p'))
  let lnum = line('.')
  for [key, val] in items(s:breakpoints)
    if val['fname'] == fname && val['lnum'] == lnum
      call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r")
      " Assume this always wors, the reply is simply "^done".
      exe 'sign unplace ' . (s:break_id + key)
      unlet s:breakpoints[key]
      break
    endif
  endfor
endfunc

" :Next, :Continue, etc - send a command to gdb
func s:SendCommand(cmd)
  call term_sendkeys(s:commbuf, a:cmd . "\r")
endfunc

" Handle stopping and running message from gdb.
" Will update the sign that shows the current position.
func s:HandleCursor(msg)
  let wid = win_getid(winnr())

  if win_gotoid(s:startwin)
    if a:msg =~ '^\*stopped'
      let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
      let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
      if lnum =~ '^[0-9]*$'
	if expand('%:h') != fname
	  if &modified
	    " TODO: find existing window
	    exe 'split ' . fnameescape(fname)
	    let s:startwin = win_getid(winnr())
	  else
	    exe 'edit ' . fnameescape(fname)
	  endif
	endif
	exe lnum
	exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fnameescape(fname)
	setlocal signcolumn=yes
      endif
    else
      exe 'sign unplace ' . s:pc_id
    endif

    call win_gotoid(wid)
  endif
endfunc

" Handle setting a breakpoint
" Will update the sign that shows the breakpoint
func s:HandleNewBreakpoint(msg)
  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
  if nr == 0
    return
  endif

  if has_key(s:breakpoints, nr)
    let entry = s:breakpoints[nr]
  else
    let entry = {}
    let s:breakpoints[nr] = entry
  endif

  let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')

  exe 'sign place ' . (s:break_id + nr) . ' line=' . lnum . ' name=debugBreakpoint file=' . fnameescape(fname)

  let entry['fname'] = fname
  let entry['lnum'] = lnum
endfunc

" Handle deleting a breakpoint
" Will remove the sign that shows the breakpoint
func s:HandleBreakpointDelete(msg)
  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
  if nr == 0
    return
  endif
  exe 'sign unplace ' . (s:break_id + nr)
  unlet s:breakpoints[nr]
endfunc