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