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