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