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