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  let save_cpo = &cpo
570  set cpo&vim
571
572  command Break call s:SetBreakpoint()
573  command Clear call s:ClearBreakpoint()
574  command Step call s:SendCommand('-exec-step')
575  command Over call s:SendCommand('-exec-next')
576  command Finish call s:SendCommand('-exec-finish')
577  command -nargs=* Run call s:Run(<q-args>)
578  command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
579  command Stop call s:SendCommand('-exec-interrupt')
580
581  " using -exec-continue results in CTRL-C in gdb window not working
582  if s:way == 'prompt'
583    command Continue call s:SendCommand('continue')
584  else
585    command Continue call term_sendkeys(s:gdbbuf, "continue\r")
586  endif
587
588  command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
589  command Gdb call win_gotoid(s:gdbwin)
590  command Program call win_gotoid(s:ptywin)
591  command Source call s:GotoSourcewinOrCreateIt()
592  command Winbar call s:InstallWinbar()
593
594  " TODO: can the K mapping be restored?
595  nnoremap K :Evaluate<CR>
596
597  if has('menu') && &mouse != ''
598    call s:InstallWinbar()
599
600    if !exists('g:termdebug_popup') || g:termdebug_popup != 0
601      let s:saved_mousemodel = &mousemodel
602      let &mousemodel = 'popup_setpos'
603      an 1.200 PopUp.-SEP3-	<Nop>
604      an 1.210 PopUp.Set\ breakpoint	:Break<CR>
605      an 1.220 PopUp.Clear\ breakpoint	:Clear<CR>
606      an 1.230 PopUp.Evaluate		:Evaluate<CR>
607    endif
608  endif
609
610  let &cpo = save_cpo
611endfunc
612
613let s:winbar_winids = []
614
615" Install the window toolbar in the current window.
616func s:InstallWinbar()
617  if has('menu') && &mouse != ''
618    nnoremenu WinBar.Step   :Step<CR>
619    nnoremenu WinBar.Next   :Over<CR>
620    nnoremenu WinBar.Finish :Finish<CR>
621    nnoremenu WinBar.Cont   :Continue<CR>
622    nnoremenu WinBar.Stop   :Stop<CR>
623    nnoremenu WinBar.Eval   :Evaluate<CR>
624    call add(s:winbar_winids, win_getid(winnr()))
625  endif
626endfunc
627
628" Delete installed debugger commands in the current window.
629func s:DeleteCommands()
630  delcommand Break
631  delcommand Clear
632  delcommand Step
633  delcommand Over
634  delcommand Finish
635  delcommand Run
636  delcommand Arguments
637  delcommand Stop
638  delcommand Continue
639  delcommand Evaluate
640  delcommand Gdb
641  delcommand Program
642  delcommand Source
643  delcommand Winbar
644
645  nunmap K
646
647  if has('menu')
648    " Remove the WinBar entries from all windows where it was added.
649    let curwinid = win_getid(winnr())
650    for winid in s:winbar_winids
651      if win_gotoid(winid)
652	aunmenu WinBar.Step
653	aunmenu WinBar.Next
654	aunmenu WinBar.Finish
655	aunmenu WinBar.Cont
656	aunmenu WinBar.Stop
657	aunmenu WinBar.Eval
658      endif
659    endfor
660    call win_gotoid(curwinid)
661    let s:winbar_winids = []
662
663    if exists('s:saved_mousemodel')
664      let &mousemodel = s:saved_mousemodel
665      unlet s:saved_mousemodel
666      aunmenu PopUp.-SEP3-
667      aunmenu PopUp.Set\ breakpoint
668      aunmenu PopUp.Clear\ breakpoint
669      aunmenu PopUp.Evaluate
670    endif
671  endif
672
673  exe 'sign unplace ' . s:pc_id
674  for key in keys(s:breakpoints)
675    exe 'sign unplace ' . (s:break_id + key)
676  endfor
677  unlet s:breakpoints
678
679  sign undefine debugPC
680  for val in s:BreakpointSigns
681    exe "sign undefine debugBreakpoint" . val
682  endfor
683  let s:BreakpointSigns = []
684endfunc
685
686" :Break - Set a breakpoint at the cursor position.
687func s:SetBreakpoint()
688  " Setting a breakpoint may not work while the program is running.
689  " Interrupt to make it work.
690  let do_continue = 0
691  if !s:stopped
692    let do_continue = 1
693    if s:way == 'prompt'
694      call s:PromptInterrupt()
695    else
696      call s:SendCommand('-exec-interrupt')
697    endif
698    sleep 10m
699  endif
700  " Use the fname:lnum format, older gdb can't handle --source.
701  call s:SendCommand('-break-insert '
702	\ . fnameescape(expand('%:p')) . ':' . line('.'))
703  if do_continue
704    call s:SendCommand('-exec-continue')
705  endif
706endfunc
707
708" :Clear - Delete a breakpoint at the cursor position.
709func s:ClearBreakpoint()
710  let fname = fnameescape(expand('%:p'))
711  let lnum = line('.')
712  for [key, val] in items(s:breakpoints)
713    if val['fname'] == fname && val['lnum'] == lnum
714      call s:SendCommand('-break-delete ' . key)
715      " Assume this always wors, the reply is simply "^done".
716      exe 'sign unplace ' . (s:break_id + key)
717      unlet s:breakpoints[key]
718      break
719    endif
720  endfor
721endfunc
722
723func s:Run(args)
724  if a:args != ''
725    call s:SendCommand('-exec-arguments ' . a:args)
726  endif
727  call s:SendCommand('-exec-run')
728endfunc
729
730func s:SendEval(expr)
731  call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
732  let s:evalexpr = a:expr
733endfunc
734
735" :Evaluate - evaluate what is under the cursor
736func s:Evaluate(range, arg)
737  if a:arg != ''
738    let expr = a:arg
739  elseif a:range == 2
740    let pos = getcurpos()
741    let reg = getreg('v', 1, 1)
742    let regt = getregtype('v')
743    normal! gv"vy
744    let expr = @v
745    call setpos('.', pos)
746    call setreg('v', reg, regt)
747  else
748    let expr = expand('<cexpr>')
749  endif
750  let s:ignoreEvalError = 0
751  call s:SendEval(expr)
752endfunc
753
754let s:ignoreEvalError = 0
755let s:evalFromBalloonExpr = 0
756
757" Handle the result of data-evaluate-expression
758func s:HandleEvaluate(msg)
759  let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
760  let value = substitute(value, '\\"', '"', 'g')
761  if s:evalFromBalloonExpr
762    if s:evalFromBalloonExprResult == ''
763      let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
764    else
765      let s:evalFromBalloonExprResult .= ' = ' . value
766    endif
767    call balloon_show(s:evalFromBalloonExprResult)
768  else
769    echomsg '"' . s:evalexpr . '": ' . value
770  endif
771
772  if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
773    " Looks like a pointer, also display what it points to.
774    let s:ignoreEvalError = 1
775    call s:SendEval('*' . s:evalexpr)
776  else
777    let s:evalFromBalloonExpr = 0
778  endif
779endfunc
780
781" Show a balloon with information of the variable under the mouse pointer,
782" if there is any.
783func TermDebugBalloonExpr()
784  if v:beval_winid != s:sourcewin
785    return
786  endif
787  if !s:stopped
788    " Only evaluate when stopped, otherwise setting a breakpoint using the
789    " mouse triggers a balloon.
790    return
791  endif
792  let s:evalFromBalloonExpr = 1
793  let s:evalFromBalloonExprResult = ''
794  let s:ignoreEvalError = 1
795  call s:SendEval(v:beval_text)
796  return ''
797endfunc
798
799" Handle an error.
800func s:HandleError(msg)
801  if s:ignoreEvalError
802    " Result of s:SendEval() failed, ignore.
803    let s:ignoreEvalError = 0
804    let s:evalFromBalloonExpr = 0
805    return
806  endif
807  echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
808endfunc
809
810func s:GotoSourcewinOrCreateIt()
811  if !win_gotoid(s:sourcewin)
812    new
813    let s:sourcewin = win_getid(winnr())
814    call s:InstallWinbar()
815  endif
816endfunc
817
818" Handle stopping and running message from gdb.
819" Will update the sign that shows the current position.
820func s:HandleCursor(msg)
821  let wid = win_getid(winnr())
822
823  if a:msg =~ '^\*stopped'
824    call ch_log('program stopped')
825    let s:stopped = 1
826  elseif a:msg =~ '^\*running'
827    call ch_log('program running')
828    let s:stopped = 0
829  endif
830
831  if a:msg =~ 'fullname='
832    let fname = s:GetFullname(a:msg)
833  else
834    let fname = ''
835  endif
836  if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
837    let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
838    if lnum =~ '^[0-9]*$'
839    call s:GotoSourcewinOrCreateIt()
840      if expand('%:p') != fnamemodify(fname, ':p')
841	if &modified
842	  " TODO: find existing window
843	  exe 'split ' . fnameescape(fname)
844	  let s:sourcewin = win_getid(winnr())
845	  call s:InstallWinbar()
846	else
847	  exe 'edit ' . fnameescape(fname)
848	endif
849      endif
850      exe lnum
851      exe 'sign unplace ' . s:pc_id
852      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
853      setlocal signcolumn=yes
854    endif
855  elseif !s:stopped || fname != ''
856    exe 'sign unplace ' . s:pc_id
857  endif
858
859  call win_gotoid(wid)
860endfunc
861
862let s:BreakpointSigns = []
863
864func s:CreateBreakpoint(nr)
865  if index(s:BreakpointSigns, a:nr) == -1
866    call add(s:BreakpointSigns, a:nr)
867    exe "sign define debugBreakpoint" . a:nr . " text=" . a:nr . " texthl=debugBreakpoint"
868  endif
869endfunc
870
871" Handle setting a breakpoint
872" Will update the sign that shows the breakpoint
873func s:HandleNewBreakpoint(msg)
874  if a:msg !~ 'fullname='
875    " a watch does not have a file name
876    return
877  endif
878
879  let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
880  if nr == 0
881    return
882  endif
883  call s:CreateBreakpoint(nr)
884
885  if has_key(s:breakpoints, nr)
886    let entry = s:breakpoints[nr]
887  else
888    let entry = {}
889    let s:breakpoints[nr] = entry
890  endif
891
892  let fname = s:GetFullname(a:msg)
893  let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
894  let entry['fname'] = fname
895  let entry['lnum'] = lnum
896
897  if bufloaded(fname)
898    call s:PlaceSign(nr, entry)
899  endif
900endfunc
901
902func s:PlaceSign(nr, entry)
903  exe 'sign place ' . (s:break_id + a:nr) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . a:nr . ' file=' . a:entry['fname']
904  let a:entry['placed'] = 1
905endfunc
906
907" Handle deleting a breakpoint
908" Will remove the sign that shows the breakpoint
909func s:HandleBreakpointDelete(msg)
910  let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
911  if nr == 0
912    return
913  endif
914  if has_key(s:breakpoints, nr)
915    let entry = s:breakpoints[nr]
916    if has_key(entry, 'placed')
917      exe 'sign unplace ' . (s:break_id + nr)
918      unlet entry['placed']
919    endif
920    unlet s:breakpoints[nr]
921  endif
922endfunc
923
924" Handle the debugged program starting to run.
925" Will store the process ID in s:pid
926func s:HandleProgramRun(msg)
927  let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
928  if nr == 0
929    return
930  endif
931  let s:pid = nr
932  call ch_log('Detected process ID: ' . s:pid)
933endfunc
934
935" Handle a BufRead autocommand event: place any signs.
936func s:BufRead()
937  let fname = expand('<afile>:p')
938  for [nr, entry] in items(s:breakpoints)
939    if entry['fname'] == fname
940      call s:PlaceSign(nr, entry)
941    endif
942  endfor
943endfunc
944
945" Handle a BufUnloaded autocommand event: unplace any signs.
946func s:BufUnloaded()
947  let fname = expand('<afile>:p')
948  for [nr, entry] in items(s:breakpoints)
949    if entry['fname'] == fname
950      let entry['placed'] = 0
951    endif
952  endfor
953endfunc
954
955let &cpo = s:keepcpo
956unlet s:keepcpo
957