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