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