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