1" Debugger plugin using gdb.
2"
3" Author: Bram Moolenaar
4" Copyright: Vim license applies, see ":help license"
5" Last Update: 2018 Jun 3
6"
7" WORK IN PROGRESS - Only the basics work
8" Note: On MS-Windows you need a recent version of gdb.  The one included with
9" MingW is too old (7.6.1).
10" I used version 7.12 from http://www.equation.com/servlet/equation.cmd?fa=gdb
11"
12" There are two ways to run gdb:
13" - In a terminal window; used if possible, does not work on MS-Windows
14"   Not used when g:termdebug_use_prompt is set to 1.
15" - Using a "prompt" buffer; may use a terminal window for the program
16"
17" For both the current window is used to view source code and shows the
18" current statement from gdb.
19"
20" USING A TERMINAL WINDOW
21"
22" Opens two visible terminal windows:
23" 1. runs a pty for the debugged program, as with ":term NONE"
24" 2. runs gdb, passing the pty of the debugged program
25" A third terminal window is hidden, it is used for communication with gdb.
26"
27" USING A PROMPT BUFFER
28"
29" Opens a window with a prompt buffer to communicate with gdb.
30" Gdb is run as a job with callbacks for I/O.
31" On Unix another terminal window is opened to run the debugged program
32" On MS-Windows a separate console is opened to run the debugged program
33"
34" The communication with gdb uses GDB/MI.  See:
35" https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html
36
37" In case this gets sourced twice.
38if exists(':Termdebug')
39  finish
40endif
41
42" Need either the +terminal feature or +channel and the prompt buffer.
43" The terminal feature does not work with gdb on win32.
44if has('terminal') && !has('win32')
45  let s:way = 'terminal'
46elseif has('channel') && exists('*prompt_setprompt')
47  let s:way = 'prompt'
48else
49  if has('terminal')
50    let s:err = 'Cannot debug, missing prompt buffer support'
51  else
52    let s:err = 'Cannot debug, +channel feature is not supported'
53  endif
54  command -nargs=* -complete=file -bang Termdebug echoerr s:err
55  command -nargs=+ -complete=file -bang TermdebugCommand echoerr s:err
56  finish
57endif
58
59let s:keepcpo = &cpo
60set cpo&vim
61
62" The command that starts debugging, e.g. ":Termdebug vim".
63" To end type "quit" in the gdb window.
64command -nargs=* -complete=file -bang Termdebug call s:StartDebug(<bang>0, <f-args>)
65command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>)
66
67" Name of the gdb command, defaults to "gdb".
68if !exists('termdebugger')
69  let termdebugger = 'gdb'
70endif
71
72let s:pc_id = 12
73let s:break_id = 13  " breakpoint number is added to this
74let s:stopped = 1
75
76" Take a breakpoint number as used by GDB and turn it into an integer.
77" The breakpoint may contain a dot: 123.4 -> 123004
78" The main breakpoint has a zero subid.
79func s:Breakpoint2SignNumber(id, subid)
80  return s:break_id + a:id * 1000 + a:subid
81endfunction
82
83func s:Highlight(init, old, new)
84  let default = a:init ? 'default ' : ''
85  if a:new ==# 'light' && a:old !=# 'light'
86    exe "hi " . default . "debugPC term=reverse ctermbg=lightblue guibg=lightblue"
87  elseif a:new ==# 'dark' && a:old !=# 'dark'
88    exe "hi " . default . "debugPC term=reverse ctermbg=darkblue guibg=darkblue"
89  endif
90endfunc
91
92call s:Highlight(1, '', &background)
93hi default debugBreakpoint term=reverse ctermbg=red guibg=red
94
95func s:StartDebug(bang, ...)
96  " First argument is the command to debug, second core file or process ID.
97  call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang})
98endfunc
99
100func s:StartDebugCommand(bang, ...)
101  " First argument is the command to debug, rest are run arguments.
102  call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang})
103endfunc
104
105func s:StartDebug_internal(dict)
106  if exists('s:gdbwin')
107    echoerr 'Terminal debugger already running'
108    return
109  endif
110  let s:ptywin = 0
111  let s:pid = 0
112
113  " Uncomment this line to write logging in "debuglog".
114  " call ch_logfile('debuglog', 'w')
115
116  let s:sourcewin = win_getid(winnr())
117  let s:startsigncolumn = &signcolumn
118
119  let s:save_columns = 0
120  if exists('g:termdebug_wide')
121    if &columns < g:termdebug_wide
122      let s:save_columns = &columns
123      let &columns = g:termdebug_wide
124    endif
125    let s:vertical = 1
126  else
127    let s:vertical = 0
128  endif
129
130  " Override using a terminal window by setting g:termdebug_use_prompt to 1.
131  let use_prompt = exists('g:termdebug_use_prompt') && g:termdebug_use_prompt
132  if has('terminal') && !has('win32') && !use_prompt
133    let s:way = 'terminal'
134  else
135    let s:way = 'prompt'
136  endif
137
138  if s:way == 'prompt'
139    call s:StartDebug_prompt(a:dict)
140  else
141    call s:StartDebug_term(a:dict)
142  endif
143endfunc
144
145func s:StartDebug_term(dict)
146  " Open a terminal window without a job, to run the debugged program in.
147  let s:ptybuf = term_start('NONE', {
148        \ 'term_name': 'debugged program',
149        \ 'vertical': s:vertical,
150        \ })
151  if s:ptybuf == 0
152    echoerr 'Failed to open the program terminal window'
153    return
154  endif
155  let pty = job_info(term_getjob(s:ptybuf))['tty_out']
156  let s:ptywin = win_getid(winnr())
157  if s:vertical
158    " Assuming the source code window will get a signcolumn, use two more
159    " columns for that, thus one less for the terminal window.
160    exe (&columns / 2 - 1) . "wincmd |"
161  endif
162
163  " Create a hidden terminal window to communicate with gdb
164  let s:commbuf = term_start('NONE', {
165        \ 'term_name': 'gdb communication',
166        \ 'out_cb': function('s:CommOutput'),
167        \ 'hidden': 1,
168        \ })
169  if s:commbuf == 0
170    echoerr 'Failed to open the communication terminal window'
171    exe 'bwipe! ' . s:ptybuf
172    return
173  endif
174  let commpty = job_info(term_getjob(s:commbuf))['tty_out']
175
176  " Open a terminal window to run the debugger.
177  " Add -quiet to avoid the intro message causing a hit-enter prompt.
178  let gdb_args = get(a:dict, 'gdb_args', [])
179  let proc_args = get(a:dict, 'proc_args', [])
180
181  let cmd = [g:termdebugger, '-quiet', '-tty', pty] + gdb_args
182  call ch_log('executing "' . join(cmd) . '"')
183  let s:gdbbuf = term_start(cmd, {
184        \ 'exit_cb': function('s:EndTermDebug'),
185        \ 'term_finish': 'close',
186        \ })
187  if s:gdbbuf == 0
188    echoerr 'Failed to open the gdb terminal window'
189    exe 'bwipe! ' . s:ptybuf
190    exe 'bwipe! ' . s:commbuf
191    return
192  endif
193  let s:gdbwin = win_getid(winnr())
194
195  " Set arguments to be run
196  if len(proc_args)
197    call term_sendkeys(s:gdbbuf, 'set args ' . join(proc_args) . "\r")
198  endif
199
200  " Connect gdb to the communication pty, using the GDB/MI interface
201  call term_sendkeys(s:gdbbuf, 'new-ui mi ' . commpty . "\r")
202
203  " Wait for the response to show up, users may not notice the error and wonder
204  " why the debugger doesn't work.
205  let try_count = 0
206  while 1
207    let response = ''
208    for lnum in range(1,200)
209      if term_getline(s:gdbbuf, lnum) =~ 'new-ui mi '
210        " response can be in the same line or the next line
211        let response = term_getline(s:gdbbuf, lnum) . term_getline(s:gdbbuf, lnum + 1)
212        if response =~ 'Undefined command'
213          echoerr 'Sorry, your gdb is too old, gdb 7.12 is required'
214          exe 'bwipe! ' . s:ptybuf
215          exe 'bwipe! ' . s:commbuf
216          return
217        endif
218        if response =~ 'New UI allocated'
219          " Success!
220          break
221        endif
222      endif
223    endfor
224    if response =~ 'New UI allocated'
225      break
226    endif
227    let try_count += 1
228    if try_count > 100
229      echoerr 'Cannot check if your gdb works, continuing anyway'
230      break
231    endif
232    sleep 10m
233  endwhile
234
235  " Interpret commands while the target is running.  This should usualy only be
236  " exec-interrupt, since many commands don't work properly while the target is
237  " running.
238  call s:SendCommand('-gdb-set mi-async on')
239  " Older gdb uses a different command.
240  call s:SendCommand('-gdb-set target-async on')
241
242  " Disable pagination, it causes everything to stop at the gdb
243  " "Type <return> to continue" prompt.
244  call s:SendCommand('set pagination off')
245
246  call s:StartDebugCommon(a:dict)
247endfunc
248
249func s:StartDebug_prompt(dict)
250  " Open a window with a prompt buffer to run gdb in.
251  if s:vertical
252    vertical new
253  else
254    new
255  endif
256  let s:gdbwin = win_getid(winnr())
257  let s:promptbuf = bufnr('')
258  call prompt_setprompt(s:promptbuf, 'gdb> ')
259  set buftype=prompt
260  file gdb
261  call prompt_setcallback(s:promptbuf, function('s:PromptCallback'))
262  call prompt_setinterrupt(s:promptbuf, function('s:PromptInterrupt'))
263
264  if s:vertical
265    " Assuming the source code window will get a signcolumn, use two more
266    " columns for that, thus one less for the terminal window.
267    exe (&columns / 2 - 1) . "wincmd |"
268  endif
269
270  " Add -quiet to avoid the intro message causing a hit-enter prompt.
271  let gdb_args = get(a:dict, 'gdb_args', [])
272  let proc_args = get(a:dict, 'proc_args', [])
273
274  let cmd = [g:termdebugger, '-quiet', '--interpreter=mi2'] + gdb_args
275  call ch_log('executing "' . join(cmd) . '"')
276
277  let s:gdbjob = job_start(cmd, {
278        \ 'exit_cb': function('s:EndPromptDebug'),
279        \ 'out_cb': function('s:GdbOutCallback'),
280        \ })
281  if job_status(s:gdbjob) != "run"
282    echoerr 'Failed to start gdb'
283    exe 'bwipe! ' . s:promptbuf
284    return
285  endif
286  " Mark the buffer modified so that it's not easy to close.
287  set modified
288  let s:gdb_channel = job_getchannel(s:gdbjob)
289
290  " Interpret commands while the target is running.  This should usualy only
291  " be exec-interrupt, since many commands don't work properly while the
292  " target is running.
293  call s:SendCommand('-gdb-set mi-async on')
294  " Older gdb uses a different command.
295  call s:SendCommand('-gdb-set target-async on')
296
297  let s:ptybuf = 0
298  if has('win32')
299    " MS-Windows: run in a new console window for maximum compatibility
300    call s:SendCommand('set new-console on')
301  elseif has('terminal')
302    " Unix: Run the debugged program in a terminal window.  Open it below the
303    " gdb window.
304    belowright let s:ptybuf = term_start('NONE', {
305          \ 'term_name': 'debugged program',
306          \ })
307    if s:ptybuf == 0
308      echoerr 'Failed to open the program terminal window'
309      call job_stop(s:gdbjob)
310      return
311    endif
312    let s:ptywin = win_getid(winnr())
313    let pty = job_info(term_getjob(s:ptybuf))['tty_out']
314    call s:SendCommand('tty ' . pty)
315
316    " Since GDB runs in a prompt window, the environment has not been set to
317    " match a terminal window, need to do that now.
318    call s:SendCommand('set env TERM = xterm-color')
319    call s:SendCommand('set env ROWS = ' . winheight(s:ptywin))
320    call s:SendCommand('set env LINES = ' . winheight(s:ptywin))
321    call s:SendCommand('set env COLUMNS = ' . winwidth(s:ptywin))
322    call s:SendCommand('set env COLORS = ' . &t_Co)
323    call s:SendCommand('set env VIM_TERMINAL = ' . v:version)
324  else
325    " TODO: open a new terminal get get the tty name, pass on to gdb
326    call s:SendCommand('show inferior-tty')
327  endif
328  call s:SendCommand('set print pretty on')
329  call s:SendCommand('set breakpoint pending on')
330  " Disable pagination, it causes everything to stop at the gdb
331  call s:SendCommand('set pagination off')
332
333  " Set arguments to be run
334  if len(proc_args)
335    call s:SendCommand('set args ' . join(proc_args))
336  endif
337
338  call s:StartDebugCommon(a:dict)
339  startinsert
340endfunc
341
342func s:StartDebugCommon(dict)
343  " Sign used to highlight the line where the program has stopped.
344  " There can be only one.
345  sign define debugPC linehl=debugPC
346
347  " Install debugger commands in the text window.
348  call win_gotoid(s:sourcewin)
349  call s:InstallCommands()
350  call win_gotoid(s:gdbwin)
351
352  " Enable showing a balloon with eval info
353  if has("balloon_eval") || has("balloon_eval_term")
354    set balloonexpr=TermDebugBalloonExpr()
355    if has("balloon_eval")
356      set ballooneval
357    endif
358    if has("balloon_eval_term")
359      set balloonevalterm
360    endif
361  endif
362
363  " Contains breakpoints that have been placed, key is a string with the GDB
364  " breakpoint number.
365  " Each entry is a dict, containing the sub-breakpoints.  Key is the subid.
366  " For a breakpoint that is just a number the subid is zero.
367  " For a breakpoint "123.4" the id is "123" and subid is "4".
368  " Example, when breakpoint "44", "123", "123.1" and "123.2" exist:
369  " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}}
370  let s:breakpoints = {}
371
372  " Contains breakpoints by file/lnum.  The key is "fname:lnum".
373  " Each entry is a list of breakpoint IDs at that position.
374  let s:breakpoint_locations = {}
375
376  augroup TermDebug
377    au BufRead * call s:BufRead()
378    au BufUnload * call s:BufUnloaded()
379    au OptionSet background call s:Highlight(0, v:option_old, v:option_new)
380  augroup END
381
382  " Run the command if the bang attribute was given and got to the debug
383  " window.
384  if get(a:dict, 'bang', 0)
385    call s:SendCommand('-exec-run')
386    call win_gotoid(s:ptywin)
387  endif
388endfunc
389
390" Send a command to gdb.  "cmd" is the string without line terminator.
391func s:SendCommand(cmd)
392  call ch_log('sending to gdb: ' . a:cmd)
393  if s:way == 'prompt'
394    call ch_sendraw(s:gdb_channel, a:cmd . "\n")
395  else
396    call term_sendkeys(s:commbuf, a:cmd . "\r")
397  endif
398endfunc
399
400" This is global so that a user can create their mappings with this.
401func TermDebugSendCommand(cmd)
402  if s:way == 'prompt'
403    call ch_sendraw(s:gdb_channel, a:cmd . "\n")
404  else
405    let do_continue = 0
406    if !s:stopped
407      let do_continue = 1
408      call s:SendCommand('-exec-interrupt')
409      sleep 10m
410    endif
411    call term_sendkeys(s:gdbbuf, a:cmd . "\r")
412    if do_continue
413      Continue
414    endif
415  endif
416endfunc
417
418" Function called when entering a line in the prompt buffer.
419func s:PromptCallback(text)
420  call s:SendCommand(a:text)
421endfunc
422
423" Function called when pressing CTRL-C in the prompt buffer and when placing a
424" breakpoint.
425func s:PromptInterrupt()
426  call ch_log('Interrupting gdb')
427  if has('win32')
428    " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to
429    " the debugger program so that gdb responds again.
430    if s:pid == 0
431      echoerr 'Cannot interrupt gdb, did not find a process ID'
432    else
433      call debugbreak(s:pid)
434    endif
435  else
436    call job_stop(s:gdbjob, 'int')
437  endif
438endfunc
439
440" Function called when gdb outputs text.
441func s:GdbOutCallback(channel, text)
442  call ch_log('received from gdb: ' . a:text)
443
444  " Drop the gdb prompt, we have our own.
445  " Drop status and echo'd commands.
446  if a:text == '(gdb) ' || a:text == '^done' || a:text[0] == '&'
447    return
448  endif
449  if a:text =~ '^^error,msg='
450    let text = s:DecodeMessage(a:text[11:])
451    if exists('s:evalexpr') && text =~ 'A syntax error in expression, near\|No symbol .* in current context'
452      " Silently drop evaluation errors.
453      unlet s:evalexpr
454      return
455    endif
456  elseif a:text[0] == '~'
457    let text = s:DecodeMessage(a:text[1:])
458  else
459    call s:CommOutput(a:channel, a:text)
460    return
461  endif
462
463  let curwinid = win_getid(winnr())
464  call win_gotoid(s:gdbwin)
465
466  " Add the output above the current prompt.
467  call append(line('$') - 1, text)
468  set modified
469
470  call win_gotoid(curwinid)
471endfunc
472
473" Decode a message from gdb.  quotedText starts with a ", return the text up
474" to the next ", unescaping characters.
475func s:DecodeMessage(quotedText)
476  if a:quotedText[0] != '"'
477    echoerr 'DecodeMessage(): missing quote in ' . a:quotedText
478    return
479  endif
480  let result = ''
481  let i = 1
482  while a:quotedText[i] != '"' && i < len(a:quotedText)
483    if a:quotedText[i] == '\'
484      let i += 1
485      if a:quotedText[i] == 'n'
486        " drop \n
487        let i += 1
488        continue
489      endif
490    endif
491    let result .= a:quotedText[i]
492    let i += 1
493  endwhile
494  return result
495endfunc
496
497" Extract the "name" value from a gdb message with fullname="name".
498func s:GetFullname(msg)
499  if a:msg !~ 'fullname'
500    return ''
501  endif
502  let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', ''))
503  if has('win32') && name =~ ':\\\\'
504    " sometimes the name arrives double-escaped
505    let name = substitute(name, '\\\\', '\\', 'g')
506  endif
507  return name
508endfunc
509
510func s:EndTermDebug(job, status)
511  exe 'bwipe! ' . s:commbuf
512  unlet s:gdbwin
513
514  call s:EndDebugCommon()
515endfunc
516
517func s:EndDebugCommon()
518  let curwinid = win_getid(winnr())
519
520  if exists('s:ptybuf') && s:ptybuf
521    exe 'bwipe! ' . s:ptybuf
522  endif
523
524  call win_gotoid(s:sourcewin)
525  let &signcolumn = s:startsigncolumn
526  call s:DeleteCommands()
527
528  call win_gotoid(curwinid)
529
530  if s:save_columns > 0
531    let &columns = s:save_columns
532  endif
533
534  if has("balloon_eval") || has("balloon_eval_term")
535    set balloonexpr=
536    if has("balloon_eval")
537      set noballooneval
538    endif
539    if has("balloon_eval_term")
540      set noballoonevalterm
541    endif
542  endif
543
544  au! TermDebug
545endfunc
546
547func s:EndPromptDebug(job, status)
548  let curwinid = win_getid(winnr())
549  call win_gotoid(s:gdbwin)
550  set nomodified
551  close
552  if curwinid != s:gdbwin
553    call win_gotoid(curwinid)
554  endif
555
556  call s:EndDebugCommon()
557  unlet s:gdbwin
558  call ch_log("Returning from EndPromptDebug()")
559endfunc
560
561" Handle a message received from gdb on the GDB/MI interface.
562func s:CommOutput(chan, msg)
563  let msgs = split(a:msg, "\r")
564
565  for msg in msgs
566    " remove prefixed NL
567    if msg[0] == "\n"
568      let msg = msg[1:]
569    endif
570    if msg != ''
571      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
572        call s:HandleCursor(msg)
573      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
574        call s:HandleNewBreakpoint(msg)
575      elseif msg =~ '^=breakpoint-deleted,'
576        call s:HandleBreakpointDelete(msg)
577      elseif msg =~ '^=thread-group-started'
578        call s:HandleProgramRun(msg)
579      elseif msg =~ '^\^done,value='
580        call s:HandleEvaluate(msg)
581      elseif msg =~ '^\^error,msg='
582        call s:HandleError(msg)
583      endif
584    endif
585  endfor
586endfunc
587
588" Install commands in the current window to control the debugger.
589func s:InstallCommands()
590  let save_cpo = &cpo
591  set cpo&vim
592
593  command Break call s:SetBreakpoint()
594  command Clear call s:ClearBreakpoint()
595  command Step call s:SendCommand('-exec-step')
596  command Over call s:SendCommand('-exec-next')
597  command Finish call s:SendCommand('-exec-finish')
598  command -nargs=* Run call s:Run(<q-args>)
599  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
600  command Stop call s:SendCommand('-exec-interrupt')
601
602  " using -exec-continue results in CTRL-C in gdb window not working
603  if s:way == 'prompt'
604    command Continue call s:SendCommand('continue')
605  else
606    command Continue call term_sendkeys(s:gdbbuf, "continue\r")
607  endif
608
609  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
610  command Gdb call win_gotoid(s:gdbwin)
611  command Program call win_gotoid(s:ptywin)
612  command Source call s:GotoSourcewinOrCreateIt()
613  command Winbar call s:InstallWinbar()
614
615  " TODO: can the K mapping be restored?
616  nnoremap K :Evaluate<CR>
617
618  if has('menu') && &mouse != ''
619    call s:InstallWinbar()
620
621    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
622      let s:saved_mousemodel = &mousemodel
623      let &mousemodel = 'popup_setpos'
624      an 1.200 PopUp.-SEP3-	<Nop>
625      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
626      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
627      an 1.230 PopUp.Evaluate		:Evaluate<CR>
628    endif
629  endif
630
631  let &cpo = save_cpo
632endfunc
633
634let s:winbar_winids = []
635
636" Install the window toolbar in the current window.
637func s:InstallWinbar()
638  if has('menu') && &mouse != ''
639    nnoremenu WinBar.Step   :Step<CR>
640    nnoremenu WinBar.Next   :Over<CR>
641    nnoremenu WinBar.Finish :Finish<CR>
642    nnoremenu WinBar.Cont   :Continue<CR>
643    nnoremenu WinBar.Stop   :Stop<CR>
644    nnoremenu WinBar.Eval   :Evaluate<CR>
645    call add(s:winbar_winids, win_getid(winnr()))
646  endif
647endfunc
648
649" Delete installed debugger commands in the current window.
650func s:DeleteCommands()
651  delcommand Break
652  delcommand Clear
653  delcommand Step
654  delcommand Over
655  delcommand Finish
656  delcommand Run
657  delcommand Arguments
658  delcommand Stop
659  delcommand Continue
660  delcommand Evaluate
661  delcommand Gdb
662  delcommand Program
663  delcommand Source
664  delcommand Winbar
665
666  nunmap K
667
668  if has('menu')
669    " Remove the WinBar entries from all windows where it was added.
670    let curwinid = win_getid(winnr())
671    for winid in s:winbar_winids
672      if win_gotoid(winid)
673        aunmenu WinBar.Step
674        aunmenu WinBar.Next
675        aunmenu WinBar.Finish
676        aunmenu WinBar.Cont
677        aunmenu WinBar.Stop
678        aunmenu WinBar.Eval
679      endif
680    endfor
681    call win_gotoid(curwinid)
682    let s:winbar_winids = []
683
684    if exists('s:saved_mousemodel')
685      let &mousemodel = s:saved_mousemodel
686      unlet s:saved_mousemodel
687      aunmenu PopUp.-SEP3-
688      aunmenu PopUp.Set\ breakpoint
689      aunmenu PopUp.Clear\ breakpoint
690      aunmenu PopUp.Evaluate
691    endif
692  endif
693
694  exe 'sign unplace ' . s:pc_id
695  for [id, entries] in items(s:breakpoints)
696    for subid in keys(entries)
697      exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
698    endfor
699  endfor
700  unlet s:breakpoints
701  unlet s:breakpoint_locations
702
703  sign undefine debugPC
704  for val in s:BreakpointSigns
705    exe "sign undefine debugBreakpoint" . val
706  endfor
707  let s:BreakpointSigns = []
708endfunc
709
710" :Break - Set a breakpoint at the cursor position.
711func s:SetBreakpoint()
712  " Setting a breakpoint may not work while the program is running.
713  " Interrupt to make it work.
714  let do_continue = 0
715  if !s:stopped
716    let do_continue = 1
717    if s:way == 'prompt'
718      call s:PromptInterrupt()
719    else
720      call s:SendCommand('-exec-interrupt')
721    endif
722    sleep 10m
723  endif
724  " Use the fname:lnum format, older gdb can't handle --source.
725  call s:SendCommand('-break-insert '
726        \ . fnameescape(expand('%:p')) . ':' . line('.'))
727  if do_continue
728    call s:SendCommand('-exec-continue')
729  endif
730endfunc
731
732" :Clear - Delete a breakpoint at the cursor position.
733func s:ClearBreakpoint()
734  let fname = fnameescape(expand('%:p'))
735  let lnum = line('.')
736  let bploc = printf('%s:%d', fname, lnum)
737  if has_key(s:breakpoint_locations, bploc)
738    let idx = 0
739    for id in s:breakpoint_locations[bploc]
740      if has_key(s:breakpoints, id)
741        " Assume this always works, the reply is simply "^done".
742        call s:SendCommand('-break-delete ' . id)
743        for subid in keys(s:breakpoints[id])
744          exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
745        endfor
746        unlet s:breakpoints[id]
747        unlet s:breakpoint_locations[bploc][idx]
748        break
749      else
750	let idx += 1
751      endif
752    endfor
753    if empty(s:breakpoint_locations[bploc])
754      unlet s:breakpoint_locations[bploc]
755    endif
756  endif
757endfunc
758
759func s:Run(args)
760  if a:args != ''
761    call s:SendCommand('-exec-arguments ' . a:args)
762  endif
763  call s:SendCommand('-exec-run')
764endfunc
765
766func s:SendEval(expr)
767  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
768  let s:evalexpr = a:expr
769endfunc
770
771" :Evaluate - evaluate what is under the cursor
772func s:Evaluate(range, arg)
773  if a:arg != ''
774    let expr = a:arg
775  elseif a:range == 2
776    let pos = getcurpos()
777    let reg = getreg('v', 1, 1)
778    let regt = getregtype('v')
779    normal! gv"vy
780    let expr = @v
781    call setpos('.', pos)
782    call setreg('v', reg, regt)
783  else
784    let expr = expand('<cexpr>')
785  endif
786  let s:ignoreEvalError = 0
787  call s:SendEval(expr)
788endfunc
789
790let s:ignoreEvalError = 0
791let s:evalFromBalloonExpr = 0
792
793" Handle the result of data-evaluate-expression
794func s:HandleEvaluate(msg)
795  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
796  let value = substitute(value, '\\"', '"', 'g')
797  if s:evalFromBalloonExpr
798    if s:evalFromBalloonExprResult == ''
799      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
800    else
801      let s:evalFromBalloonExprResult .= ' = ' . value
802    endif
803    call balloon_show(s:evalFromBalloonExprResult)
804  else
805    echomsg '"' . s:evalexpr . '": ' . value
806  endif
807
808  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
809    " Looks like a pointer, also display what it points to.
810    let s:ignoreEvalError = 1
811    call s:SendEval('*' . s:evalexpr)
812  else
813    let s:evalFromBalloonExpr = 0
814  endif
815endfunc
816
817" Show a balloon with information of the variable under the mouse pointer,
818" if there is any.
819func TermDebugBalloonExpr()
820  if v:beval_winid != s:sourcewin
821    return
822  endif
823  if !s:stopped
824    " Only evaluate when stopped, otherwise setting a breakpoint using the
825    " mouse triggers a balloon.
826    return
827  endif
828  let s:evalFromBalloonExpr = 1
829  let s:evalFromBalloonExprResult = ''
830  let s:ignoreEvalError = 1
831  call s:SendEval(v:beval_text)
832  return ''
833endfunc
834
835" Handle an error.
836func s:HandleError(msg)
837  if s:ignoreEvalError
838    " Result of s:SendEval() failed, ignore.
839    let s:ignoreEvalError = 0
840    let s:evalFromBalloonExpr = 0
841    return
842  endif
843  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
844endfunc
845
846func s:GotoSourcewinOrCreateIt()
847  if !win_gotoid(s:sourcewin)
848    new
849    let s:sourcewin = win_getid(winnr())
850    call s:InstallWinbar()
851  endif
852endfunc
853
854" Handle stopping and running message from gdb.
855" Will update the sign that shows the current position.
856func s:HandleCursor(msg)
857  let wid = win_getid(winnr())
858
859  if a:msg =~ '^\*stopped'
860    call ch_log('program stopped')
861    let s:stopped = 1
862  elseif a:msg =~ '^\*running'
863    call ch_log('program running')
864    let s:stopped = 0
865  endif
866
867  if a:msg =~ 'fullname='
868    let fname = s:GetFullname(a:msg)
869  else
870    let fname = ''
871  endif
872  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
873    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
874    if lnum =~ '^[0-9]*$'
875    call s:GotoSourcewinOrCreateIt()
876      if expand('%:p') != fnamemodify(fname, ':p')
877        if &modified
878          " TODO: find existing window
879          exe 'split ' . fnameescape(fname)
880          let s:sourcewin = win_getid(winnr())
881          call s:InstallWinbar()
882        else
883          exe 'edit ' . fnameescape(fname)
884        endif
885      endif
886      exe lnum
887      exe 'sign unplace ' . s:pc_id
888      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
889      setlocal signcolumn=yes
890    endif
891  elseif !s:stopped || fname != ''
892    exe 'sign unplace ' . s:pc_id
893  endif
894
895  call win_gotoid(wid)
896endfunc
897
898let s:BreakpointSigns = []
899
900func s:CreateBreakpoint(id, subid)
901  let nr = printf('%d.%d', a:id, a:subid)
902  if index(s:BreakpointSigns, nr) == -1
903    call add(s:BreakpointSigns, nr)
904    exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint"
905  endif
906endfunc
907
908func! s:SplitMsg(s)
909  return split(a:s, '{.\{-}}\zs')
910endfunction
911
912" Handle setting a breakpoint
913" Will update the sign that shows the breakpoint
914func s:HandleNewBreakpoint(msg)
915  if a:msg !~ 'fullname='
916    " a watch does not have a file name
917    return
918  endif
919  for msg in s:SplitMsg(a:msg)
920    let fname = s:GetFullname(msg)
921    if empty(fname)
922      continue
923    endif
924    let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '')
925    if empty(nr)
926      return
927    endif
928
929    " If "nr" is 123 it becomes "123.0" and subid is "0".
930    " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded.
931    let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0')
932    call s:CreateBreakpoint(id, subid)
933
934    if has_key(s:breakpoints, id)
935      let entries = s:breakpoints[id]
936    else
937      let entries = {}
938      let s:breakpoints[id] = entries
939    endif
940    if has_key(entries, subid)
941      let entry = entries[subid]
942    else
943      let entry = {}
944      let entries[subid] = entry
945    endif
946
947    let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
948    let entry['fname'] = fname
949    let entry['lnum'] = lnum
950
951    let bploc = printf('%s:%d', fname, lnum)
952    if !has_key(s:breakpoint_locations, bploc)
953      let s:breakpoint_locations[bploc] = []
954    endif
955    let s:breakpoint_locations[bploc] += [id]
956
957    if bufloaded(fname)
958      call s:PlaceSign(id, subid, entry)
959    endif
960  endfor
961endfunc
962
963func s:PlaceSign(id, subid, entry)
964  let nr = printf('%d.%d', a:id, a:subid)
965  exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' file=' . a:entry['fname']
966  let a:entry['placed'] = 1
967endfunc
968
969" Handle deleting a breakpoint
970" Will remove the sign that shows the breakpoint
971func s:HandleBreakpointDelete(msg)
972  let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
973  if empty(id)
974    return
975  endif
976  if has_key(s:breakpoints, id)
977    for [subid, entry] in items(s:breakpoints[id])
978      if has_key(entry, 'placed')
979        exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
980        unlet entry['placed']
981      endif
982    endfor
983    unlet s:breakpoints[id]
984  endif
985endfunc
986
987" Handle the debugged program starting to run.
988" Will store the process ID in s:pid
989func s:HandleProgramRun(msg)
990  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
991  if nr == 0
992    return
993  endif
994  let s:pid = nr
995  call ch_log('Detected process ID: ' . s:pid)
996endfunc
997
998" Handle a BufRead autocommand event: place any signs.
999func s:BufRead()
1000  let fname = expand('<afile>:p')
1001  for [id, entries] in items(s:breakpoints)
1002    for [subid, entry] in items(entries)
1003      if entry['fname'] == fname
1004        call s:PlaceSign(id, subid, entry)
1005      endif
1006    endfor
1007  endfor
1008endfunc
1009
1010" Handle a BufUnloaded autocommand event: unplace any signs.
1011func s:BufUnloaded()
1012  let fname = expand('<afile>:p')
1013  for [id, entries] in items(s:breakpoints)
1014    for [subid, entry] in items(entries)
1015      if entry['fname'] == fname
1016        let entry['placed'] = 0
1017      endif
1018    endfor
1019  endfor
1020endfunc
1021
1022let &cpo = s:keepcpo
1023unlet s:keepcpo
1024