1" Debugger plugin using gdb.
2"
3" WORK IN PROGRESS - much doesn't work yet
4"
5" Open two visible terminal windows:
6" 1. run a pty, as with ":term NONE"
7" 2. run gdb, passing the pty
8" The current window is used to view source code and follows gdb.
9"
10" A third terminal window is hidden, it is used for communication with gdb.
11"
12" The communication with gdb uses GDB/MI.  See:
13" https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html
14"
15" Author: Bram Moolenaar
16" Copyright: Vim license applies, see ":help license"
17
18" The command that starts debugging, e.g. ":Termdebug vim".
19" To end type "quit" in the gdb window.
20command -nargs=* -complete=file Termdebug call s:StartDebug(<q-args>)
21
22" Name of the gdb command, defaults to "gdb".
23if !exists('debugger')
24  let debugger = 'gdb'
25endif
26
27" Sign used to highlight the line where the program has stopped.
28sign define debugPC linehl=debugPC
29if &background == 'light'
30  hi debugPC term=reverse ctermbg=lightblue guibg=lightblue
31else
32  hi debugPC term=reverse ctermbg=darkblue guibg=darkblue
33endif
34let s:pc_id = 12
35
36func s:StartDebug(cmd)
37  let s:startwin = win_getid(winnr())
38  let s:startsigncolumn = &signcolumn
39
40  " Open a terminal window without a job, to run the debugged program
41  let s:ptybuf = term_start('NONE', {
42	\ 'term_name': 'gdb program',
43	\ })
44  if s:ptybuf == 0
45    echoerr 'Failed to open the program terminal window'
46    return
47  endif
48  let pty = job_info(term_getjob(s:ptybuf))['tty_out']
49
50  " Create a hidden terminal window to communicate with gdb
51  let s:commbuf = term_start('NONE', {
52	\ 'term_name': 'gdb communication',
53	\ 'out_cb': function('s:CommOutput'),
54	\ 'hidden': 1,
55	\ })
56  if s:commbuf == 0
57    echoerr 'Failed to open the communication terminal window'
58    exe 'bwipe! ' . s:ptybuf
59    return
60  endif
61  let commpty = job_info(term_getjob(s:commbuf))['tty_out']
62
63  " Open a terminal window to run the debugger.
64  let cmd = [g:debugger, '-tty', pty, a:cmd]
65  echomsg 'executing "' . join(cmd) . '"'
66  let gdbbuf = term_start(cmd, {
67	\ 'exit_cb': function('s:EndDebug'),
68	\ 'term_finish': 'close',
69	\ })
70  if gdbbuf == 0
71    echoerr 'Failed to open the gdb terminal window'
72    exe 'bwipe! ' . s:ptybuf
73    exe 'bwipe! ' . s:commbuf
74    return
75  endif
76
77  " Connect gdb to the communication pty, using the GDB/MI interface
78  call term_sendkeys(gdbbuf, 'new-ui mi ' . commpty . "\r")
79endfunc
80
81func s:EndDebug(job, status)
82  exe 'bwipe! ' . s:ptybuf
83  exe 'bwipe! ' . s:commbuf
84  call setwinvar(s:startwin, '&signcolumn', s:startsigncolumn)
85endfunc
86
87" Handle a message received from gdb on the GDB/MI interface.
88func s:CommOutput(chan, msg)
89  let msgs = split(a:msg, "\r")
90
91  for msg in msgs
92    " remove prefixed NL
93    if msg[0] == "\n"
94      let msg = msg[1:]
95    endif
96    if msg != ''
97      if msg =~ '^\*\(stopped\|running\)'
98	let wid = win_getid(winnr())
99
100	if win_gotoid(s:startwin)
101	  if msg =~ '^\*stopped'
102	    " TODO: proper parsing
103	    let fname = substitute(msg, '.*fullname="\([^"]*\)".*', '\1', '')
104	    let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
105	    if lnum =~ '^[0-9]*$'
106	      if expand('%:h') != fname
107		if &modified
108		  " TODO: find existing window
109		  exe 'split ' . fnameescape(fname)
110		  let s:startwin = win_getid(winnr())
111		else
112		  exe 'edit ' . fnameescape(fname)
113		endif
114	      endif
115	      exe lnum
116	      exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fnameescape(fname)
117	      setlocal signcolumn=yes
118	    endif
119	  else
120	    exe 'sign unplace ' . s:pc_id
121	  endif
122
123	  call win_gotoid(wid)
124	endif
125      endif
126    endif
127  endfor
128endfunc
129