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