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