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