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  call ch_log('Interrupting gdb')
406  if has('win32')
407    " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to
408    " the debugger program so that gdb responds again.
409    if s:pid == 0
410      echoerr 'Cannot interrupt gdb, did not find a process ID'
411    else
412      call debugbreak(s:pid)
413    endif
414  else
415    call job_stop(s:gdbjob, 'int')
416  endif
417endfunc
418
419" Function called when gdb outputs text.
420func s:GdbOutCallback(channel, text)
421  call ch_log('received from gdb: ' . a:text)
422
423  " Drop the gdb prompt, we have our own.
424  " Drop status and echo'd commands.
425  if a:text == '(gdb) ' || a:text == '^done' || a:text[0] == '&'
426    return
427  endif
428  if a:text =~ '^^error,msg='
429    let text = s:DecodeMessage(a:text[11:])
430    if exists('s:evalexpr') && text =~ 'A syntax error in expression, near\|No symbol .* in current context'
431      " Silently drop evaluation errors.
432      unlet s:evalexpr
433      return
434    endif
435  elseif a:text[0] == '~'
436    let text = s:DecodeMessage(a:text[1:])
437  else
438    call s:CommOutput(a:channel, a:text)
439    return
440  endif
441
442  let curwinid = win_getid(winnr())
443  call win_gotoid(s:gdbwin)
444
445  " Add the output above the current prompt.
446  call append(line('$') - 1, text)
447  set modified
448
449  call win_gotoid(curwinid)
450endfunc
451
452" Decode a message from gdb.  quotedText starts with a ", return the text up
453" to the next ", unescaping characters.
454func s:DecodeMessage(quotedText)
455  if a:quotedText[0] != '"'
456    echoerr 'DecodeMessage(): missing quote in ' . a:quotedText
457    return
458  endif
459  let result = ''
460  let i = 1
461  while a:quotedText[i] != '"' && i < len(a:quotedText)
462    if a:quotedText[i] == '\'
463      let i += 1
464      if a:quotedText[i] == 'n'
465	" drop \n
466	let i += 1
467	continue
468      endif
469    endif
470    let result .= a:quotedText[i]
471    let i += 1
472  endwhile
473  return result
474endfunc
475
476" Extract the "name" value from a gdb message with fullname="name".
477func s:GetFullname(msg)
478  let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', ''))
479  if has('win32') && name =~ ':\\\\'
480    " sometimes the name arrives double-escaped
481    let name = substitute(name, '\\\\', '\\', 'g')
482  endif
483  return name
484endfunc
485
486func s:EndTermDebug(job, status)
487  exe 'bwipe! ' . s:commbuf
488  unlet s:gdbwin
489
490  call s:EndDebugCommon()
491endfunc
492
493func s:EndDebugCommon()
494  let curwinid = win_getid(winnr())
495
496  if exists('s:ptybuf') && s:ptybuf
497    exe 'bwipe! ' . s:ptybuf
498  endif
499
500  call win_gotoid(s:sourcewin)
501  let &signcolumn = s:startsigncolumn
502  call s:DeleteCommands()
503
504  call win_gotoid(curwinid)
505
506  if s:save_columns > 0
507    let &columns = s:save_columns
508  endif
509
510  if has("balloon_eval") || has("balloon_eval_term")
511    set balloonexpr=
512    if has("balloon_eval")
513      set noballooneval
514    endif
515    if has("balloon_eval_term")
516      set noballoonevalterm
517    endif
518  endif
519
520  au! TermDebug
521endfunc
522
523func s:EndPromptDebug(job, status)
524  let curwinid = win_getid(winnr())
525  call win_gotoid(s:gdbwin)
526  set nomodified
527  close
528  if curwinid != s:gdbwin
529    call win_gotoid(curwinid)
530  endif
531
532  call s:EndDebugCommon()
533  unlet s:gdbwin
534  call ch_log("Returning from EndPromptDebug()")
535endfunc
536
537" Handle a message received from gdb on the GDB/MI interface.
538func s:CommOutput(chan, msg)
539  let msgs = split(a:msg, "\r")
540
541  for msg in msgs
542    " remove prefixed NL
543    if msg[0] == "\n"
544      let msg = msg[1:]
545    endif
546    if msg != ''
547      if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
548	call s:HandleCursor(msg)
549      elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
550	call s:HandleNewBreakpoint(msg)
551      elseif msg =~ '^=breakpoint-deleted,'
552	call s:HandleBreakpointDelete(msg)
553      elseif msg =~ '^=thread-group-started'
554	call s:HandleProgramRun(msg)
555      elseif msg =~ '^\^done,value='
556	call s:HandleEvaluate(msg)
557      elseif msg =~ '^\^error,msg='
558	call s:HandleError(msg)
559      endif
560    endif
561  endfor
562endfunc
563
564" Install commands in the current window to control the debugger.
565func s:InstallCommands()
566  command Break call s:SetBreakpoint()
567  command Clear call s:ClearBreakpoint()
568  command Step call s:SendCommand('-exec-step')
569  command Over call s:SendCommand('-exec-next')
570  command Finish call s:SendCommand('-exec-finish')
571  command -nargs=* Run call s:Run(<q-args>)
572  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
573  command Stop call s:SendCommand('-exec-interrupt')
574
575  " using -exec-continue results in CTRL-C in gdb window not working
576  if s:way == 'prompt'
577    command Continue call s:SendCommand('continue')
578  else
579    command Continue call term_sendkeys(s:gdbbuf, "continue\r")
580  endif
581
582  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
583  command Gdb call win_gotoid(s:gdbwin)
584  command Program call win_gotoid(s:ptywin)
585  command Source call s:GotoSourcewinOrCreateIt()
586  command Winbar call s:InstallWinbar()
587
588  " TODO: can the K mapping be restored?
589  nnoremap K :Evaluate<CR>
590
591  if has('menu') && &mouse != ''
592    call s:InstallWinbar()
593
594    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
595      let s:saved_mousemodel = &mousemodel
596      let &mousemodel = 'popup_setpos'
597      an 1.200 PopUp.-SEP3-	<Nop>
598      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
599      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
600      an 1.230 PopUp.Evaluate		:Evaluate<CR>
601    endif
602  endif
603endfunc
604
605let s:winbar_winids = []
606
607" Install the window toolbar in the current window.
608func s:InstallWinbar()
609  if has('menu') && &mouse != ''
610    nnoremenu WinBar.Step   :Step<CR>
611    nnoremenu WinBar.Next   :Over<CR>
612    nnoremenu WinBar.Finish :Finish<CR>
613    nnoremenu WinBar.Cont   :Continue<CR>
614    nnoremenu WinBar.Stop   :Stop<CR>
615    nnoremenu WinBar.Eval   :Evaluate<CR>
616    call add(s:winbar_winids, win_getid(winnr()))
617  endif
618endfunc
619
620" Delete installed debugger commands in the current window.
621func s:DeleteCommands()
622  delcommand Break
623  delcommand Clear
624  delcommand Step
625  delcommand Over
626  delcommand Finish
627  delcommand Run
628  delcommand Arguments
629  delcommand Stop
630  delcommand Continue
631  delcommand Evaluate
632  delcommand Gdb
633  delcommand Program
634  delcommand Source
635  delcommand Winbar
636
637  nunmap K
638
639  if has('menu')
640    " Remove the WinBar entries from all windows where it was added.
641    let curwinid = win_getid(winnr())
642    for winid in s:winbar_winids
643      if win_gotoid(winid)
644	aunmenu WinBar.Step
645	aunmenu WinBar.Next
646	aunmenu WinBar.Finish
647	aunmenu WinBar.Cont
648	aunmenu WinBar.Stop
649	aunmenu WinBar.Eval
650      endif
651    endfor
652    call win_gotoid(curwinid)
653    let s:winbar_winids = []
654
655    if exists('s:saved_mousemodel')
656      let &mousemodel = s:saved_mousemodel
657      unlet s:saved_mousemodel
658      aunmenu PopUp.-SEP3-
659      aunmenu PopUp.Set\ breakpoint
660      aunmenu PopUp.Clear\ breakpoint
661      aunmenu PopUp.Evaluate
662    endif
663  endif
664
665  exe 'sign unplace ' . s:pc_id
666  for key in keys(s:breakpoints)
667    exe 'sign unplace ' . (s:break_id + key)
668  endfor
669  unlet s:breakpoints
670
671  sign undefine debugPC
672  for val in s:BreakpointSigns
673    exe "sign undefine debugBreakpoint" . val
674  endfor
675  let s:BreakpointSigns = []
676endfunc
677
678" :Break - Set a breakpoint at the cursor position.
679func s:SetBreakpoint()
680  " Setting a breakpoint may not work while the program is running.
681  " Interrupt to make it work.
682  let do_continue = 0
683  if !s:stopped
684    let do_continue = 1
685    if s:way == 'prompt'
686      call s:PromptInterrupt()
687    else
688      call s:SendCommand('-exec-interrupt')
689    endif
690    sleep 10m
691  endif
692  " Use the fname:lnum format, older gdb can't handle --source.
693  call s:SendCommand('-break-insert '
694	\ . fnameescape(expand('%:p')) . ':' . line('.'))
695  if do_continue
696    call s:SendCommand('-exec-continue')
697  endif
698endfunc
699
700" :Clear - Delete a breakpoint at the cursor position.
701func s:ClearBreakpoint()
702  let fname = fnameescape(expand('%:p'))
703  let lnum = line('.')
704  for [key, val] in items(s:breakpoints)
705    if val['fname'] == fname && val['lnum'] == lnum
706      call s:SendCommand('-break-delete ' . key)
707      " Assume this always wors, the reply is simply "^done".
708      exe 'sign unplace ' . (s:break_id + key)
709      unlet s:breakpoints[key]
710      break
711    endif
712  endfor
713endfunc
714
715func s:Run(args)
716  if a:args != ''
717    call s:SendCommand('-exec-arguments ' . a:args)
718  endif
719  call s:SendCommand('-exec-run')
720endfunc
721
722func s:SendEval(expr)
723  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
724  let s:evalexpr = a:expr
725endfunc
726
727" :Evaluate - evaluate what is under the cursor
728func s:Evaluate(range, arg)
729  if a:arg != ''
730    let expr = a:arg
731  elseif a:range == 2
732    let pos = getcurpos()
733    let reg = getreg('v', 1, 1)
734    let regt = getregtype('v')
735    normal! gv"vy
736    let expr = @v
737    call setpos('.', pos)
738    call setreg('v', reg, regt)
739  else
740    let expr = expand('<cexpr>')
741  endif
742  let s:ignoreEvalError = 0
743  call s:SendEval(expr)
744endfunc
745
746let s:ignoreEvalError = 0
747let s:evalFromBalloonExpr = 0
748
749" Handle the result of data-evaluate-expression
750func s:HandleEvaluate(msg)
751  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
752  let value = substitute(value, '\\"', '"', 'g')
753  if s:evalFromBalloonExpr
754    if s:evalFromBalloonExprResult == ''
755      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
756    else
757      let s:evalFromBalloonExprResult .= ' = ' . value
758    endif
759    call balloon_show(s:evalFromBalloonExprResult)
760  else
761    echomsg '"' . s:evalexpr . '": ' . value
762  endif
763
764  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
765    " Looks like a pointer, also display what it points to.
766    let s:ignoreEvalError = 1
767    call s:SendEval('*' . s:evalexpr)
768  else
769    let s:evalFromBalloonExpr = 0
770  endif
771endfunc
772
773" Show a balloon with information of the variable under the mouse pointer,
774" if there is any.
775func TermDebugBalloonExpr()
776  if v:beval_winid != s:sourcewin
777    return
778  endif
779  if !s:stopped
780    " Only evaluate when stopped, otherwise setting a breakpoint using the
781    " mouse triggers a balloon.
782    return
783  endif
784  let s:evalFromBalloonExpr = 1
785  let s:evalFromBalloonExprResult = ''
786  let s:ignoreEvalError = 1
787  call s:SendEval(v:beval_text)
788  return ''
789endfunc
790
791" Handle an error.
792func s:HandleError(msg)
793  if s:ignoreEvalError
794    " Result of s:SendEval() failed, ignore.
795    let s:ignoreEvalError = 0
796    let s:evalFromBalloonExpr = 0
797    return
798  endif
799  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
800endfunc
801
802func s:GotoSourcewinOrCreateIt()
803  if !win_gotoid(s:sourcewin)
804    new
805    let s:sourcewin = win_getid(winnr())
806    call s:InstallWinbar()
807  endif
808endfunc
809
810" Handle stopping and running message from gdb.
811" Will update the sign that shows the current position.
812func s:HandleCursor(msg)
813  let wid = win_getid(winnr())
814
815  if a:msg =~ '^\*stopped'
816    call ch_log('program stopped')
817    let s:stopped = 1
818  elseif a:msg =~ '^\*running'
819    call ch_log('program running')
820    let s:stopped = 0
821  endif
822
823  if a:msg =~ 'fullname='
824    let fname = s:GetFullname(a:msg)
825  else
826    let fname = ''
827  endif
828  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
829    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
830    if lnum =~ '^[0-9]*$'
831    call s:GotoSourcewinOrCreateIt()
832      if expand('%:p') != fnamemodify(fname, ':p')
833	if &modified
834	  " TODO: find existing window
835	  exe 'split ' . fnameescape(fname)
836	  let s:sourcewin = win_getid(winnr())
837	  call s:InstallWinbar()
838	else
839	  exe 'edit ' . fnameescape(fname)
840	endif
841      endif
842      exe lnum
843      exe 'sign unplace ' . s:pc_id
844      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
845      setlocal signcolumn=yes
846    endif
847  elseif !s:stopped || fname != ''
848    exe 'sign unplace ' . s:pc_id
849  endif
850
851  call win_gotoid(wid)
852endfunc
853
854let s:BreakpointSigns = []
855
856func s:CreateBreakpoint(nr)
857  if index(s:BreakpointSigns, a:nr) == -1
858    call add(s:BreakpointSigns, a:nr)
859    exe "sign define debugBreakpoint" . a:nr . " text=" . a:nr . " texthl=debugBreakpoint"
860  endif
861endfunc
862
863" Handle setting a breakpoint
864" Will update the sign that shows the breakpoint
865func s:HandleNewBreakpoint(msg)
866  if a:msg !~ 'fullname='
867    " a watch does not have a file name
868    return
869  endif
870
871  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
872  if nr == 0
873    return
874  endif
875  call s:CreateBreakpoint(nr)
876
877  if has_key(s:breakpoints, nr)
878    let entry = s:breakpoints[nr]
879  else
880    let entry = {}
881    let s:breakpoints[nr] = entry
882  endif
883
884  let fname = s:GetFullname(a:msg)
885  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
886  let entry['fname'] = fname
887  let entry['lnum'] = lnum
888
889  if bufloaded(fname)
890    call s:PlaceSign(nr, entry)
891  endif
892endfunc
893
894func s:PlaceSign(nr, entry)
895  exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . a:nr . ' file=' . a:entry['fname']
896  let a:entry['placed'] = 1
897endfunc
898
899" Handle deleting a breakpoint
900" Will remove the sign that shows the breakpoint
901func s:HandleBreakpointDelete(msg)
902  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
903  if nr == 0
904    return
905  endif
906  if has_key(s:breakpoints, nr)
907    let entry = s:breakpoints[nr]
908    if has_key(entry, 'placed')
909      exe 'sign unplace ' . (s:break_id + nr)
910      unlet entry['placed']
911    endif
912    unlet s:breakpoints[nr]
913  endif
914endfunc
915
916" Handle the debugged program starting to run.
917" Will store the process ID in s:pid
918func s:HandleProgramRun(msg)
919  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
920  if nr == 0
921    return
922  endif
923  let s:pid = nr
924  call ch_log('Detected process ID: ' . s:pid)
925endfunc
926
927" Handle a BufRead autocommand event: place any signs.
928func s:BufRead()
929  let fname = expand('<afile>:p')
930  for [nr, entry] in items(s:breakpoints)
931    if entry['fname'] == fname
932      call s:PlaceSign(nr, entry)
933    endif
934  endfor
935endfunc
936
937" Handle a BufUnloaded autocommand event: unplace any signs.
938func s:BufUnloaded()
939  let fname = expand('<afile>:p')
940  for [nr, entry] in items(s:breakpoints)
941    if entry['fname'] == fname
942      let entry['placed'] = 0
943    endif
944  endfor
945endfunc
946