aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/nvim_terminal_emulator.txt253
-rw-r--r--runtime/pack/dist/opt/termdebug/plugin/termdebug.vim827
2 files changed, 956 insertions, 124 deletions
diff --git a/runtime/doc/nvim_terminal_emulator.txt b/runtime/doc/nvim_terminal_emulator.txt
index cfaec12520..fe5506fc4c 100644
--- a/runtime/doc/nvim_terminal_emulator.txt
+++ b/runtime/doc/nvim_terminal_emulator.txt
@@ -131,4 +131,257 @@ a local 'statusline'. Example: >
:autocmd TermOpen * setlocal statusline=%{b:term_title}
<
==============================================================================
+5. Debugging *terminal-debug* *terminal-debugger*
+
+The Terminal debugging plugin can be used to debug a program with gdb and view
+the source code in a Vim window. Since this is completely contained inside
+Vim this also works remotely over an ssh connection.
+
+Starting ~
+ *termdebug-starting*
+Load the plugin with this command: >
+ packadd termdebug
+< *:Termdebug*
+To start debugging use `:Termdebug` or `:TermdebugCommand` followed by the
+command name, for example: >
+ :Termdebug vim
+
+This opens two windows:
+
+gdb window A terminal window in which "gdb vim" is executed. Here you
+ can directly interact with gdb. The buffer name is "!gdb".
+
+program window A terminal window for the executed program. When "run" is
+ used in gdb the program I/O will happen in this window, so
+ that it does not interfere with controlling gdb. The buffer
+ name is "gdb program".
+
+The current window is used to show the source code. When gdb pauses the
+source file location will be displayed, if possible. A sign is used to
+highlight the current position, using highlight group debugPC.
+
+If the buffer in the current window is modified, another window will be opened
+to display the current gdb position.
+
+Focus the terminal of the executed program to interact with it. This works
+the same as any command running in a terminal window.
+
+When the debugger ends, typically by typing "quit" in the gdb window, the two
+opened windows are closed.
+
+Only one debugger can be active at a time.
+ *:TermdebugCommand*
+If you want to give specific commands to the command being debugged, you can
+use the `:TermdebugCommand` command followed by the command name and
+additional parameters. >
+ :TermdebugCommand vim --clean -c ':set nu'
+
+Both the `:Termdebug` and `:TermdebugCommand` support an optional "!" bang
+argument to start the command right away, without pausing at the gdb window
+(and cursor will be in the debugged window). For example: >
+ :TermdebugCommand! vim --clean
+
+To attach gdb to an already running executable or use a core file, pass extra
+arguments. E.g.: >
+ :Termdebug vim core
+ :Termdebug vim 98343
+
+If no argument is given, you'll end up in a gdb window, in which you need to
+specify which command to run using e.g. the gdb `file` command.
+
+
+Example session ~
+ *termdebug-example*
+Start in the Vim "src" directory and build Vim: >
+ % make
+Start Vim: >
+ % ./vim
+Load the termdebug plugin and start debugging Vim: >
+ :packadd termdebug
+ :Termdebug vim
+You should now have three windows:
+ source - where you started
+ gdb - you can type gdb commands here
+ program - the executed program will use this window
+
+Put focus on the gdb window and type: >
+ break ex_help
+ run
+Vim will start running in the program window. Put focus there and type: >
+ :help gui
+Gdb will run into the ex_help breakpoint. The source window now shows the
+ex_cmds.c file. A red "1 " marker will appear in the signcolumn where the
+breakpoint was set. The line where the debugger stopped is highlighted. You
+can now step through the program. You will see the highlighting move as the
+debugger executes a line of source code.
+
+Run ":Next" a few times until the for loop is highlighted. Put the cursor on
+the end of "eap->arg", then call ":Eval". You will see this displayed:
+ "eap->arg": 0x555555e68855 "gui" ~
+This way you can inspect the value of local variables. You can also focus the
+gdb window and use a "print" command, e.g.: >
+ print *eap
+If mouse pointer movements are working, Vim will also show a balloon when the
+mouse rests on text that can be evaluated by gdb.
+You can also use the "K" mapping that will either use neovim floating windows
+if available to show the results or print below the status bar.
+
+Now go back to the source window and put the cursor on the first line after
+the for loop, then type: >
+ :Break
+You will see a "1" marker appear, this indicates the new breakpoint. Now
+run ":Cont" command and the code until the breakpoint will be executed.
+
+You can type more advanced commands in the gdb window. For example, type: >
+ watch curbuf
+Now run ":Cont" (or type "cont" in the gdb window). Execution
+will now continue until the value of "curbuf" changes, which is in do_ecmd().
+To remove this watchpoint again type in the gdb window: >
+ delete 3
+
+You can see the stack by typing in the gdb window: >
+ where
+Move through the stack frames, e.g. with: >
+ frame 3
+The source window will show the code, at the point where the call was made to
+a deeper level.
+
+
+Stepping through code ~
+ *termdebug-stepping*
+Put focus on the gdb window to type commands there. Some common ones are:
+- CTRL-C interrupt the program
+- next execute the current line and stop at the next line
+- step execute the current line and stop at the next statement,
+ entering functions
+- finish execute until leaving the current function
+- where show the stack
+- frame N go to the Nth stack frame
+- continue continue execution
+
+ *:Run* *:Arguments*
+In the window showing the source code these commands can be used to control
+gdb:
+ `:Run` [args] run the program with [args] or the previous arguments
+ `:Arguments` {args} set arguments for the next `:Run`
+
+ *:Break* set a breakpoint at the current line; a sign will be displayed
+ *:Clear* delete the breakpoint at the current line
+
+ *:Step* execute the gdb "step" command
+ *:Over* execute the gdb "next" command (`:Next` is a Vim command)
+ *:Finish* execute the gdb "finish" command
+ *:Continue* execute the gdb "continue" command
+ *:Stop* interrupt the program
+
+If gdb stops at a source line and there is no window currently showing the
+source code, a new window will be created for the source code. This also
+happens if the buffer in the source code window has been modified and can't be
+abandoned.
+
+Gdb gives each breakpoint a number. In Vim the number shows up in the sign
+column, with a red background. You can use these gdb commands:
+- info break list breakpoints
+- delete N delete breakpoint N
+You can also use the `:Clear` command if the cursor is in the line with the
+breakpoint, or use the "Clear breakpoint" right-click menu entry.
+
+
+Inspecting variables ~
+ *termdebug-variables* *:Evaluate*
+ `:Evaluate` evaluate the expression under the cursor
+ `K` same
+ `:Evaluate` {expr} evaluate {expr}
+ `:'<,'>Evaluate` evaluate the Visually selected text
+
+This is similar to using "print" in the gdb window.
+You can usually shorten `:Evaluate` to `:Ev`.
+
+
+Other commands ~
+ *termdebug-commands*
+ *:Gdb* jump to the gdb window
+ *:Program* jump to the window with the running program
+ *:Source* jump to the window with the source code, create it if there
+ isn't one
+
+
+Communication ~
+ *termdebug-communication*
+There is another, hidden, buffer, which is used for Vim to communicate with
+gdb. The buffer name is "gdb communication". Do not delete this buffer, it
+will break the debugger.
+
+Gdb has some weird behavior, the plugin does its best to work around that.
+For example, after typing "continue" in the gdb window a CTRL-C can be used to
+interrupt the running program. But after using the MI command
+"-exec-continue" pressing CTRL-C does not interrupt. Therefore you will see
+"continue" being used for the `:Continue` command, instead of using the
+communication channel.
+
+
+Customizing ~
+
+GDB command *termdebug-customizing*
+
+To change the name of the gdb command, set the "termdebugger" variable before
+invoking `:Termdebug`: >
+ let termdebugger = "mygdb"
+
+To use neovim floating windows for previewing variable evaluation, set the
+`g:termdebug_useFloatingHover` variable like this: >
+ let g:termdebug_useFloatingHover = 1
+
+If you are a mouse person, you can also define a mapping using your right
+click to one of the terminal command like evaluate the variable under the
+cursor: >
+ nnoremap <RightMouse> :Evaluate<CR>
+or set/unset a breakpoint: >
+ nnoremap <RightMouse> :Break<CR>
+
+< *gdb-version*
+Only debuggers fully compatible with gdb will work. Vim uses the GDB/MI
+interface. The "new-ui" command requires gdb version 7.12 or later. if you
+get this error:
+ Undefined command: "new-ui". Try "help".~
+Then your gdb is too old.
+
+
+Colors *hl-debugPC* *hl-debugBreakpoint*
+
+The color of the signs can be adjusted with these highlight groups:
+- debugPC the current position
+- debugBreakpoint a breakpoint
+
+The defaults are, when 'background' is "light":
+ hi debugPC term=reverse ctermbg=lightblue guibg=lightblue
+ hi debugBreakpoint term=reverse ctermbg=red guibg=red
+
+When 'background' is "dark":
+ hi debugPC term=reverse ctermbg=darkblue guibg=darkblue
+ hi debugBreakpoint term=reverse ctermbg=red guibg=red
+
+
+Shorcuts *termdebug_shortcuts*
+
+You can define your own shortcuts (mappings) to control gdb, that can work in
+any window, using the TermDebugSendCommand() function. Example: >
+ map ,w :call TermDebugSendCommand('where')<CR>
+The argument is the gdb command.
+
+
+Vim window width *termdebug_wide*
+
+To change the width of the Vim window when debugging starts, and use a
+vertical split: >
+ let g:termdebug_wide = 163
+This will set &columns to 163 when `:Termdebug` is used. The value is restored
+when quitting the debugger.
+If g:termdebug_wide is set and &columns is already larger than
+g:termdebug_wide then a vertical split will be used without changing &columns.
+Set it to 1 to get a vertical split without every changing &columns (useful
+for when the terminal can't be resized by Vim).
+
+
+
vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
index 78b1ae8ce8..f809c238f6 100644
--- a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
+++ b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
@@ -1,28 +1,70 @@
" Debugger plugin using gdb.
"
-" WORK IN PROGRESS - much doesn't work yet
+" Author: Bram Moolenaar
+" Copyright: Vim license applies, see ":help license"
+" Last Update: 2018 Jun 3
+"
+" WORK IN PROGRESS - Only the basics work
+" Note: On MS-Windows you need a recent version of gdb. The one included with
+" MingW is too old (7.6.1).
+" I used version 7.12 from http://www.equation.com/servlet/equation.cmd?fa=gdb
"
-" Open two visible terminal windows:
-" 1. run a pty, as with ":term NONE"
-" 2. run gdb, passing the pty
-" The current window is used to view source code and follows gdb.
+" There are two ways to run gdb:
+" - In a terminal window; used if possible, does not work on MS-Windows
+" Not used when g:termdebug_use_prompt is set to 1.
+" - Using a "prompt" buffer; may use a terminal window for the program
"
+" For both the current window is used to view source code and shows the
+" current statement from gdb.
+"
+" USING A TERMINAL WINDOW
+"
+" Opens two visible terminal windows:
+" 1. runs a pty for the debugged program, as with ":term NONE"
+" 2. runs gdb, passing the pty of the debugged program
" A third terminal window is hidden, it is used for communication with gdb.
"
+" USING A PROMPT BUFFER
+"
+" Opens a window with a prompt buffer to communicate with gdb.
+" Gdb is run as a job with callbacks for I/O.
+" On Unix another terminal window is opened to run the debugged program
+" On MS-Windows a separate console is opened to run the debugged program
+"
" The communication with gdb uses GDB/MI. See:
" https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html
"
+" For neovim compatibility, the vim specific calls were replaced with neovim
+" specific calls:
+" term_start -> term_open
+" term_sendkeys -> jobsend
+" term_getline -> getbufline
+" job_info && term_getjob -> using linux command ps to get the tty
+" balloon -> nvim floating window
+"
+" The code for opening the floating window was taken from the beautiful
+" implementation of LanguageClient-Neovim:
+" https://github.com/autozimu/LanguageClient-neovim/blob/0ed9b69dca49c415390a8317b19149f97ae093fa/autoload/LanguageClient.vim#L304
+"
+" Neovim terminal also works seamlessly on windows, which is why the ability
+" to use the prompt buffer was removed.
+"
" Author: Bram Moolenaar
" Copyright: Vim license applies, see ":help license"
-" In case this gets loaded twice.
+" In case this gets sourced twice.
if exists(':Termdebug')
finish
endif
+
+let s:keepcpo = &cpo
+set cpo&vim
+
" The command that starts debugging, e.g. ":Termdebug vim".
" To end type "quit" in the gdb window.
-command -nargs=* -complete=file Termdebug call s:StartDebug(<q-args>)
+command -nargs=* -complete=file -bang Termdebug call s:StartDebug(<bang>0, <f-args>)
+command -nargs=+ -complete=file -bang TermdebugCommand call s:StartDebugCommand(<bang>0, <f-args>)
" Name of the gdb command, defaults to "gdb".
if !exists('termdebugger')
@@ -30,123 +72,333 @@ if !exists('termdebugger')
endif
let s:pc_id = 12
-let s:break_id = 13
+let s:break_id = 13 " breakpoint number is added to this
+let s:stopped = 1
+
+" Take a breakpoint number as used by GDB and turn it into an integer.
+" The breakpoint may contain a dot: 123.4 -> 123004
+" The main breakpoint has a zero subid.
+func s:Breakpoint2SignNumber(id, subid)
+ return s:break_id + a:id * 1000 + a:subid
+endfunction
+
+func s:Highlight(init, old, new)
+ let default = a:init ? 'default ' : ''
+ if a:new ==# 'light' && a:old !=# 'light'
+ exe "hi " . default . "debugPC term=reverse ctermbg=lightblue guibg=lightblue"
+ elseif a:new ==# 'dark' && a:old !=# 'dark'
+ exe "hi " . default . "debugPC term=reverse ctermbg=darkblue guibg=darkblue"
+ endif
+endfunc
-if &background == 'light'
- hi default debugPC term=reverse ctermbg=lightblue guibg=lightblue
-else
- hi default debugPC term=reverse ctermbg=darkblue guibg=darkblue
-endif
+call s:Highlight(1, '', &background)
hi default debugBreakpoint term=reverse ctermbg=red guibg=red
-func s:StartDebug(cmd)
- let s:startwin = win_getid(winnr())
+func s:StartDebug(bang, ...)
+ " First argument is the command to debug, second core file or process ID.
+ call s:StartDebug_internal({'gdb_args': a:000, 'bang': a:bang})
+endfunc
+
+func s:StartDebugCommand(bang, ...)
+ " First argument is the command to debug, rest are run arguments.
+ call s:StartDebug_internal({'gdb_args': [a:1], 'proc_args': a:000[1:], 'bang': a:bang})
+endfunc
+
+func s:StartDebug_internal(dict)
+ if exists('s:gdbwin')
+ echoerr 'Terminal debugger already running'
+ return
+ endif
+ let s:ptywin = 0
+ let s:pid = 0
+
+ " Uncomment this line to write logging in "debuglog".
+ " call ch_logfile('debuglog', 'w')
+
+ let s:sourcewin = win_getid(winnr())
let s:startsigncolumn = &signcolumn
- if exists('g:termdebug_wide') && &columns < g:termdebug_wide
- let s:save_columns = &columns
- let &columns = g:termdebug_wide
- let vertical = 1
+ let s:save_columns = 0
+ if exists('g:termdebug_wide')
+ if &columns < g:termdebug_wide
+ let s:save_columns = &columns
+ let &columns = g:termdebug_wide
+ endif
+ let s:vertical = 1
else
- let s:save_columns = 0
- let vertical = 0
+ let s:vertical = 0
endif
- " Open a terminal window without a job, to run the debugged program
- let s:ptybuf = term_start('NONE', {
- \ 'term_name': 'gdb program',
- \ 'vertical': vertical,
- \ })
- if s:ptybuf == 0
+ call s:StartDebug_term(a:dict)
+endfunc
+
+" Use when debugger didn't start or ended.
+func s:CloseBuffers()
+ exe 'bwipe! ' . s:ptybuf
+ unlet! s:gdbwin
+endfunc
+
+func s:StartDebug_term(dict)
+ " Open a terminal window without a job, to run the debugged program in.
+ execute 'new'
+ let s:pty_job_id = termopen('tail -f /dev/null;#gdb program')
+ if s:pty_job_id == 0
+ echoerr 'invalid argument (or job table is full) while opening terminal window'
+ return
+ elseif s:pty_job_id == -1
echoerr 'Failed to open the program terminal window'
return
endif
- let pty = job_info(term_getjob(s:ptybuf))['tty_out']
+ let pty_job_info = nvim_get_chan_info(s:pty_job_id)
+ let s:ptybuf = pty_job_info['buffer']
+ let pty = pty_job_info['pty']
let s:ptywin = win_getid(winnr())
+ if s:vertical
+ " Assuming the source code window will get a signcolumn, use two more
+ " columns for that, thus one less for the terminal window.
+ exe (&columns / 2 - 1) . "wincmd |"
+ endif
" Create a hidden terminal window to communicate with gdb
- let s:commbuf = term_start('NONE', {
- \ 'term_name': 'gdb communication',
- \ 'out_cb': function('s:CommOutput'),
- \ 'hidden': 1,
- \ })
- if s:commbuf == 0
+ let s:comm_job_id = jobstart('tail -f /dev/null;#gdb communication', {
+ \ 'on_stdout': function('s:CommOutput'),
+ \ 'pty': v:true,
+ \ })
+ " hide terminal buffer
+ if s:comm_job_id == 0
+ echoerr 'invalid argument (or job table is full) while opening communication terminal window'
+ exe 'bwipe! ' . s:ptybuf
+ return
+ elseif s:comm_job_id == -1
echoerr 'Failed to open the communication terminal window'
exe 'bwipe! ' . s:ptybuf
return
endif
- let commpty = job_info(term_getjob(s:commbuf))['tty_out']
+ let comm_job_info = nvim_get_chan_info(s:comm_job_id)
+ let commpty = comm_job_info['pty']
" Open a terminal window to run the debugger.
- let cmd = [g:termdebugger, '-tty', pty, a:cmd]
- echomsg 'executing "' . join(cmd) . '"'
- let gdbbuf = term_start(cmd, {
- \ 'exit_cb': function('s:EndDebug'),
- \ 'term_finish': 'close',
- \ })
- if gdbbuf == 0
- echoerr 'Failed to open the gdb terminal window'
+ " Add -quiet to avoid the intro message causing a hit-enter prompt.
+ let gdb_args = get(a:dict, 'gdb_args', [])
+ let proc_args = get(a:dict, 'proc_args', [])
+
+ let cmd = [g:termdebugger, '-quiet', '-tty', pty] + gdb_args
+ "call ch_log('executing "' . join(cmd) . '"')
+ execute 'new'
+ let s:gdb_job_id = termopen(cmd, {'on_exit': function('s:EndTermDebug')})
+ if s:gdb_job_id == 0
+ echoerr 'invalid argument (or job table is full) while opening gdb terminal window'
exe 'bwipe! ' . s:ptybuf
- exe 'bwipe! ' . s:commbuf
+ return
+ elseif s:gdb_job_id == -1
+ echoerr 'Failed to open the gdb terminal window'
+ call s:CloseBuffers()
return
endif
+ let gdb_job_info = nvim_get_chan_info(s:gdb_job_id)
+ let s:gdbbuf = gdb_job_info['buffer']
let s:gdbwin = win_getid(winnr())
+ " Set arguments to be run
+ if len(proc_args)
+ call jobsend(s:gdb_job_id, 'set args ' . join(proc_args) . "\r")
+ endif
+
" Connect gdb to the communication pty, using the GDB/MI interface
- call term_sendkeys(gdbbuf, 'new-ui mi ' . commpty . "\r")
+ call jobsend(s:gdb_job_id, 'new-ui mi ' . commpty . "\r")
+
+ " Wait for the response to show up, users may not notice the error and wonder
+ " why the debugger doesn't work.
+ let try_count = 0
+ while 1
+ if nvim_get_chan_info(s:gdb_job_id) == {}
+ echoerr string(g:termdebugger) . ' exited unexpectedly'
+ call s:CloseBuffers()
+ return
+ endif
+ let response = ''
+ for lnum in range(1,200)
+ if len(getbufline(s:gdbbuf, lnum)) > 0 && getbufline(s:gdbbuf, lnum)[0] =~ 'new-ui mi '
+ " response can be in the same line or the next line
+ let response = getbufline(s:gdbbuf, lnum)[0] . getbufline(s:gdbbuf, lnum + 1)[0]
+ if response =~ 'Undefined command'
+ echoerr 'Sorry, your gdb is too old, gdb 7.12 is required'
+ call s:CloseBuffers()
+ return
+ endif
+ if response =~ 'New UI allocated'
+ " Success!
+ break
+ endif
+ endif
+ endfor
+ if response =~ 'New UI allocated'
+ break
+ endif
+ let try_count += 1
+ if try_count > 100
+ echoerr 'Cannot check if your gdb works, continuing anyway'
+ break
+ endif
+ sleep 10m
+ endwhile
+
+ " Interpret commands while the target is running. This should usualy only be
+ " exec-interrupt, since many commands don't work properly while the target is
+ " running.
+ call s:SendCommand('-gdb-set mi-async on')
+ " Older gdb uses a different command.
+ call s:SendCommand('-gdb-set target-async on')
+
+ " Disable pagination, it causes everything to stop at the gdb
+ " "Type <return> to continue" prompt.
+ call s:SendCommand('set pagination off')
+
+ call s:StartDebugCommon(a:dict)
+endfunc
+
+
+func s:StartDebugCommon(dict)
" Sign used to highlight the line where the program has stopped.
" There can be only one.
sign define debugPC linehl=debugPC
- " Sign used to indicate a breakpoint.
- " Can be used multiple times.
- sign define debugBreakpoint text=>> texthl=debugBreakpoint
-
" Install debugger commands in the text window.
- call win_gotoid(s:startwin)
+ call win_gotoid(s:sourcewin)
call s:InstallCommands()
call win_gotoid(s:gdbwin)
+ " Contains breakpoints that have been placed, key is a string with the GDB
+ " breakpoint number.
+ " Each entry is a dict, containing the sub-breakpoints. Key is the subid.
+ " For a breakpoint that is just a number the subid is zero.
+ " For a breakpoint "123.4" the id is "123" and subid is "4".
+ " Example, when breakpoint "44", "123", "123.1" and "123.2" exist:
+ " {'44': {'0': entry}, '123': {'0': entry, '1': entry, '2': entry}}
let s:breakpoints = {}
+
+ " Contains breakpoints by file/lnum. The key is "fname:lnum".
+ " Each entry is a list of breakpoint IDs at that position.
+ let s:breakpoint_locations = {}
+
+ augroup TermDebug
+ au BufRead * call s:BufRead()
+ au BufUnload * call s:BufUnloaded()
+ au OptionSet background call s:Highlight(0, v:option_old, v:option_new)
+ augroup END
+
+ " Run the command if the bang attribute was given and got to the debug
+ " window.
+ if get(a:dict, 'bang', 0)
+ call s:SendCommand('-exec-run')
+ call win_gotoid(s:ptywin)
+ endif
endfunc
-func s:EndDebug(job, status)
- exe 'bwipe! ' . s:ptybuf
- exe 'bwipe! ' . s:commbuf
+" Send a command to gdb. "cmd" is the string without line terminator.
+func s:SendCommand(cmd)
+ "call ch_log('sending to gdb: ' . a:cmd)
+ call jobsend(s:comm_job_id, a:cmd . "\r")
+endfunc
+
+" This is global so that a user can create their mappings with this.
+func TermDebugSendCommand(cmd)
+ let do_continue = 0
+ if !s:stopped
+ let do_continue = 1
+ call s:SendCommand('-exec-interrupt')
+ sleep 10m
+ endif
+ call jobsend(s:gdb_job_id, a:cmd . "\r")
+ if do_continue
+ Continue
+ endif
+endfunc
+" Decode a message from gdb. quotedText starts with a ", return the text up
+" to the next ", unescaping characters.
+func s:DecodeMessage(quotedText)
+ if a:quotedText[0] != '"'
+ echoerr 'DecodeMessage(): missing quote in ' . a:quotedText
+ return
+ endif
+ let result = ''
+ let i = 1
+ while a:quotedText[i] != '"' && i < len(a:quotedText)
+ if a:quotedText[i] == '\'
+ let i += 1
+ if a:quotedText[i] == 'n'
+ " drop \n
+ let i += 1
+ continue
+ endif
+ endif
+ let result .= a:quotedText[i]
+ let i += 1
+ endwhile
+ return result
+endfunc
+
+" Extract the "name" value from a gdb message with fullname="name".
+func s:GetFullname(msg)
+ if a:msg !~ 'fullname'
+ return ''
+ endif
+ let name = s:DecodeMessage(substitute(a:msg, '.*fullname=', '', ''))
+ if has('win32') && name =~ ':\\\\'
+ " sometimes the name arrives double-escaped
+ let name = substitute(name, '\\\\', '\\', 'g')
+ endif
+ return name
+endfunc
+
+function s:EndTermDebug(job_id, exit_code, event)
+ unlet s:gdbwin
+
+ call s:EndDebugCommon()
+endfunc
+
+func s:EndDebugCommon()
let curwinid = win_getid(winnr())
- call win_gotoid(s:startwin)
+ if exists('s:ptybuf') && s:ptybuf
+ exe 'bwipe! ' . s:ptybuf
+ endif
+
+ call win_gotoid(s:sourcewin)
let &signcolumn = s:startsigncolumn
call s:DeleteCommands()
call win_gotoid(curwinid)
+
if s:save_columns > 0
let &columns = s:save_columns
endif
+
+ au! TermDebug
endfunc
-" Handle a message received from gdb on the GDB/MI interface.
-func s:CommOutput(chan, msg)
- let msgs = split(a:msg, "\r")
+func s:CommOutput(job_id, msgs, event)
- for msg in msgs
+ for msg in a:msgs
" remove prefixed NL
if msg[0] == "\n"
let msg = msg[1:]
endif
if msg != ''
- if msg =~ '^\*\(stopped\|running\)'
- call s:HandleCursor(msg)
+ if msg =~ '^\(\*stopped\|\*running\|=thread-selected\)'
+ call s:HandleCursor(msg)
elseif msg =~ '^\^done,bkpt=' || msg =~ '^=breakpoint-created,'
- call s:HandleNewBreakpoint(msg)
+ call s:HandleNewBreakpoint(msg)
elseif msg =~ '^=breakpoint-deleted,'
- call s:HandleBreakpointDelete(msg)
+ call s:HandleBreakpointDelete(msg)
+ elseif msg =~ '^=thread-group-started'
+ call s:HandleProgramRun(msg)
elseif msg =~ '^\^done,value='
- call s:HandleEvaluate(msg)
+ call s:HandleEvaluate(msg)
elseif msg =~ '^\^error,msg='
- call s:HandleError(msg)
+ call s:HandleError(msg)
endif
endif
endfor
@@ -154,72 +406,132 @@ endfunc
" Install commands in the current window to control the debugger.
func s:InstallCommands()
+ let save_cpo = &cpo
+ set cpo&vim
+
command Break call s:SetBreakpoint()
- command Delete call s:DeleteBreakpoint()
+ command Clear call s:ClearBreakpoint()
command Step call s:SendCommand('-exec-step')
command Over call s:SendCommand('-exec-next')
command Finish call s:SendCommand('-exec-finish')
- command Continue call s:SendCommand('-exec-continue')
+ command -nargs=* Run call s:Run(<q-args>)
+ command -nargs=* Arguments call s:SendCommand('-exec-arguments ' . <q-args>)
+ command Stop call s:SendCommand('-exec-interrupt')
+
+ " using -exec-continue results in CTRL-C in gdb window not working
+ command Continue call jobsend(s:gdb_job_id, "continue\r")
+
command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>)
command Gdb call win_gotoid(s:gdbwin)
command Program call win_gotoid(s:ptywin)
+ command Source call s:GotoSourcewinOrCreateIt()
+ command Winbar call s:InstallWinbar()
" TODO: can the K mapping be restored?
nnoremap K :Evaluate<CR>
+
+ let &cpo = save_cpo
endfunc
+let s:winbar_winids = []
+
" Delete installed debugger commands in the current window.
func s:DeleteCommands()
delcommand Break
- delcommand Delete
+ delcommand Clear
delcommand Step
delcommand Over
delcommand Finish
+ delcommand Run
+ delcommand Arguments
+ delcommand Stop
delcommand Continue
delcommand Evaluate
delcommand Gdb
delcommand Program
+ delcommand Source
+ delcommand Winbar
nunmap K
+
exe 'sign unplace ' . s:pc_id
- for key in keys(s:breakpoints)
- exe 'sign unplace ' . (s:break_id + key)
+ for [id, entries] in items(s:breakpoints)
+ for subid in keys(entries)
+ exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
+ endfor
endfor
- sign undefine debugPC
- sign undefine debugBreakpoint
unlet s:breakpoints
+ unlet s:breakpoint_locations
+
+ sign undefine debugPC
+ for val in s:BreakpointSigns
+ exe "sign undefine debugBreakpoint" . val
+ endfor
+ let s:BreakpointSigns = []
endfunc
" :Break - Set a breakpoint at the cursor position.
func s:SetBreakpoint()
- call term_sendkeys(s:commbuf, '-break-insert --source '
- \ . fnameescape(expand('%:p')) . ' --line ' . line('.') . "\r")
+ " Setting a breakpoint may not work while the program is running.
+ " Interrupt to make it work.
+ let do_continue = 0
+ if !s:stopped
+ let do_continue = 1
+ call s:SendCommand('-exec-interrupt')
+ sleep 10m
+ endif
+ " Use the fname:lnum format, older gdb can't handle --source.
+ call s:SendCommand('-break-insert '
+ \ . fnameescape(expand('%:p')) . ':' . line('.'))
+ if do_continue
+ call s:SendCommand('-exec-continue')
+ endif
endfunc
-" :Delete - Delete a breakpoint at the cursor position.
-func s:DeleteBreakpoint()
+" :Clear - Delete a breakpoint at the cursor position.
+func s:ClearBreakpoint()
let fname = fnameescape(expand('%:p'))
let lnum = line('.')
- for [key, val] in items(s:breakpoints)
- if val['fname'] == fname && val['lnum'] == lnum
- call term_sendkeys(s:commbuf, '-break-delete ' . key . "\r")
- " Assume this always wors, the reply is simply "^done".
- exe 'sign unplace ' . (s:break_id + key)
- unlet s:breakpoints[key]
- break
+ let bploc = printf('%s:%d', fname, lnum)
+ if has_key(s:breakpoint_locations, bploc)
+ let idx = 0
+ for id in s:breakpoint_locations[bploc]
+ if has_key(s:breakpoints, id)
+ " Assume this always works, the reply is simply "^done".
+ call s:SendCommand('-break-delete ' . id)
+ for subid in keys(s:breakpoints[id])
+ exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
+ endfor
+ unlet s:breakpoints[id]
+ unlet s:breakpoint_locations[bploc][idx]
+ break
+ else
+ let idx += 1
+ endif
+ endfor
+ if empty(s:breakpoint_locations[bploc])
+ unlet s:breakpoint_locations[bploc]
endif
- endfor
+ endif
endfunc
-" :Next, :Continue, etc - send a command to gdb
-func s:SendCommand(cmd)
- call term_sendkeys(s:commbuf, a:cmd . "\r")
+func s:Run(args)
+ if a:args != ''
+ call s:SendCommand('-exec-arguments ' . a:args)
+ endif
+ call s:SendCommand('-exec-run')
+endfunc
+
+func s:SendEval(expr)
+ call s:SendCommand('-data-evaluate-expression "' . a:expr . '"')
+ let s:evalexpr = a:expr
endfunc
" :Evaluate - evaluate what is under the cursor
func s:Evaluate(range, arg)
if a:arg != ''
let expr = a:arg
+ let s:evalFromBalloonExpr = 0
elseif a:range == 2
let pos = getcurpos()
let reg = getreg('v', 1, 1)
@@ -228,85 +540,352 @@ func s:Evaluate(range, arg)
let expr = @v
call setpos('.', pos)
call setreg('v', reg, regt)
+ let s:evalFromBalloonExpr = 1
else
let expr = expand('<cexpr>')
+ let s:evalFromBalloonExpr = 1
endif
- call term_sendkeys(s:commbuf, '-data-evaluate-expression "' . expr . "\"\r")
- let s:evalexpr = expr
+ let s:ignoreEvalError = 0
+ call s:SendEval(expr)
endfunc
+let s:ignoreEvalError = 0
+let s:evalFromBalloonExpr = 0
+let s:evalFromBalloonExprResult = ''
+
" Handle the result of data-evaluate-expression
func s:HandleEvaluate(msg)
- echomsg '"' . s:evalexpr . '": ' . substitute(a:msg, '.*value="\(.*\)"', '\1', '')
+ let value = substitute(a:msg, '.*value="\(.*\)"', '\1', '')
+ let value = substitute(value, '\\"', '"', 'g')
+ let value = substitute(value, ' ', '\1', '')
+ if s:evalFromBalloonExpr
+ if s:evalFromBalloonExprResult == ''
+ let s:evalFromBalloonExprResult = s:evalexpr . ': ' . value
+ else
+ let s:evalFromBalloonExprResult .= ' = ' . value
+ endif
+ call s:OpenHoverPreview([s:evalFromBalloonExprResult], v:null)
+ else
+ echomsg '"' . s:evalexpr . '": ' . value
+ endif
+
+ if s:evalexpr[0] != '*' && value =~ '^0x' && value != '0x0' && value !~ '"$'
+ " Looks like a pointer, also display what it points to.
+ let s:ignoreEvalError = 1
+ call s:SendEval('*' . s:evalexpr)
+ else
+ let s:evalFromBalloonExprResult = ''
+ endif
endfunc
+function! s:ShouldUseFloatWindow() abort
+ if has('nvim_open_win') && exists('g:termdebug_useFloatingHover') && (g:termdebug_useFloatingHover == 1)
+ return v:true
+ else
+ return v:false
+ endif
+endfunction
+
+function! s:CloseFloatingHoverOnCursorMove(win_id, opened) abort
+ if getpos('.') == a:opened
+ " Just after opening floating window, CursorMoved event is run.
+ " To avoid closing floating window immediately, check the cursor
+ " was really moved
+ return
+ endif
+ autocmd! plugin-LC-neovim-close-hover
+ let winnr = win_id2win(a:win_id)
+ if winnr == 0
+ return
+ endif
+ execute winnr . 'wincmd c'
+endfunction
+
+function! s:CloseFloatingHoverOnBufEnter(win_id, bufnr) abort
+ let winnr = win_id2win(a:win_id)
+ if winnr == 0
+ " Float window was already closed
+ autocmd! plugin-LC-neovim-close-hover
+ return
+ endif
+ if winnr == winnr()
+ " Cursor is moving into floating window. Do not close it
+ return
+ endif
+ if bufnr('%') == a:bufnr
+ " When current buffer opened hover window, it's not another buffer. Skipped
+ return
+ endif
+ autocmd! plugin-LC-neovim-close-hover
+ execute winnr . 'wincmd c'
+ endfunction
+
+" Open preview window. Window is open in:
+" - Floating window on Neovim (0.4.0 or later)
+" - Preview window on Neovim (0.3.0 or earlier) or Vim
+function! s:OpenHoverPreview(lines, filetype) abort
+ " Use local variable since parameter is not modifiable
+ let lines = a:lines
+ let bufnr = bufnr('%')
+
+ let use_float_win = s:ShouldUseFloatWindow()
+ if use_float_win
+ let bufname = nvim_create_buf(v:false, v:true)
+ call nvim_buf_set_lines(buf, 0, -1, v:true, lines)
+ let pos = getpos('.')
+
+ " Calculate width and height and give margin to lines
+ let width = 0
+ for index in range(len(lines))
+ let line = lines[index]
+ if line !=# ''
+ " Give a left margin
+ let line = ' ' . line
+ endif
+ let lw = strdisplaywidth(line)
+ if lw > width
+ let width = lw
+ endif
+ let lines[index] = line
+ endfor
+
+ " Give margin
+ let width += 1
+ let lines = [''] + lines + ['']
+ let height = len(lines)
+
+ " Calculate anchor
+ " Prefer North, but if there is no space, fallback into South
+ let bottom_line = line('w0') + winheight(0) - 1
+ if pos[1] + height <= bottom_line
+ let vert = 'N'
+ let row = 1
+ else
+ let vert = 'S'
+ let row = 0
+ endif
+
+ " Prefer West, but if there is no space, fallback into East
+ if pos[2] + width <= &columns
+ let hor = 'W'
+ let col = 0
+ else
+ let hor = 'E'
+ let col = 1
+ endif
+
+ let float_win_id = nvim_open_win(bufnr, v:true, {
+ \ 'relative': 'cursor',
+ \ 'anchor': vert . hor,
+ \ 'row': row,
+ \ 'col': col,
+ \ 'width': width,
+ \ 'height': height,
+ \ })
+
+ execute 'noswapfile edit!' bufname
+
+ setlocal winhl=Normal:CursorLine
+ else
+ echomsg a:lines[0]
+ endif
+
+ if use_float_win
+ " Unlike preview window, :pclose does not close window. Instead, close
+ " hover window automatically when cursor is moved.
+ let call_after_move = printf('<SID>CloseFloatingHoverOnCursorMove(%d, %s)', float_win_id, string(pos))
+ let call_on_bufenter = printf('<SID>CloseFloatingHoverOnBufEnter(%d, %d)', float_win_id, bufnr)
+ augroup plugin-LC-neovim-close-hover
+ execute 'autocmd CursorMoved,CursorMovedI,InsertEnter <buffer> call ' . call_after_move
+ execute 'autocmd BufEnter * call ' . call_on_bufenter
+ augroup END
+ endif
+endfunction
+
" Handle an error.
func s:HandleError(msg)
+ if s:ignoreEvalError
+ " Result of s:SendEval() failed, ignore.
+ let s:ignoreEvalError = 0
+ let s:evalFromBalloonExpr = 0
+ return
+ endif
echoerr substitute(a:msg, '.*msg="\(.*\)"', '\1', '')
endfunc
+func s:GotoSourcewinOrCreateIt()
+ if !win_gotoid(s:sourcewin)
+ new
+ let s:sourcewin = win_getid(winnr())
+ call s:InstallWinbar()
+ endif
+endfunc
+
" Handle stopping and running message from gdb.
" Will update the sign that shows the current position.
func s:HandleCursor(msg)
let wid = win_getid(winnr())
- if win_gotoid(s:startwin)
- let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
- if a:msg =~ '^\*stopped' && filereadable(fname)
- let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
- if lnum =~ '^[0-9]*$'
- if expand('%:h') != fname
- if &modified
- " TODO: find existing window
- exe 'split ' . fnameescape(fname)
- let s:startwin = win_getid(winnr())
- else
- exe 'edit ' . fnameescape(fname)
- endif
- endif
- exe lnum
- exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fnameescape(fname)
- setlocal signcolumn=yes
+ if a:msg =~ '^\*stopped'
+ "call ch_log('program stopped')
+ let s:stopped = 1
+ elseif a:msg =~ '^\*running'
+ "call ch_log('program running')
+ let s:stopped = 0
+ endif
+
+ if a:msg =~ 'fullname='
+ let fname = s:GetFullname(a:msg)
+ else
+ let fname = ''
+ endif
+ if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
+ let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
+ if lnum =~ '^[0-9]*$'
+ call s:GotoSourcewinOrCreateIt()
+ if expand('%:p') != fnamemodify(fname, ':p')
+ if &modified
+ " TODO: find existing window
+ exe 'split ' . fnameescape(fname)
+ let s:sourcewin = win_getid(winnr())
+ call s:InstallWinbar()
+ else
+ exe 'edit ' . fnameescape(fname)
+ endif
endif
- else
+ exe lnum
exe 'sign unplace ' . s:pc_id
+ exe 'sign place ' . s:pc_id . ' line=' . lnum . ' name=debugPC file=' . fname
+ setlocal signcolumn=yes
endif
+ elseif !s:stopped || fname != ''
+ exe 'sign unplace ' . s:pc_id
+ endif
+
+ call win_gotoid(wid)
+endfunc
- call win_gotoid(wid)
+let s:BreakpointSigns = []
+
+func s:CreateBreakpoint(id, subid)
+ let nr = printf('%d.%d', a:id, a:subid)
+ if index(s:BreakpointSigns, nr) == -1
+ call add(s:BreakpointSigns, nr)
+ exe "sign define debugBreakpoint" . nr . " text=" . substitute(nr, '\..*', '', '') . " texthl=debugBreakpoint"
endif
endfunc
+func! s:SplitMsg(s)
+ return split(a:s, '{.\{-}}\zs')
+endfunction
+
" Handle setting a breakpoint
" Will update the sign that shows the breakpoint
func s:HandleNewBreakpoint(msg)
- let nr = substitute(a:msg, '.*number="\([0-9]\)*\".*', '\1', '') + 0
- if nr == 0
+ if a:msg !~ 'fullname='
+ " a watch does not have a file name
return
endif
+ for msg in s:SplitMsg(a:msg)
+ let fname = s:GetFullname(msg)
+ if empty(fname)
+ continue
+ endif
+ let nr = substitute(msg, '.*number="\([0-9.]*\)\".*', '\1', '')
+ if empty(nr)
+ return
+ endif
- if has_key(s:breakpoints, nr)
- let entry = s:breakpoints[nr]
- else
- let entry = {}
- let s:breakpoints[nr] = entry
- endif
+ " If "nr" is 123 it becomes "123.0" and subid is "0".
+ " If "nr" is 123.4 it becomes "123.4.0" and subid is "4"; "0" is discarded.
+ let [id, subid; _] = map(split(nr . '.0', '\.'), 'v:val + 0')
+ call s:CreateBreakpoint(id, subid)
- let fname = substitute(a:msg, '.*fullname="\([^"]*\)".*', '\1', '')
- let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '')
+ if has_key(s:breakpoints, id)
+ let entries = s:breakpoints[id]
+ else
+ let entries = {}
+ let s:breakpoints[id] = entries
+ endif
+ if has_key(entries, subid)
+ let entry = entries[subid]
+ else
+ let entry = {}
+ let entries[subid] = entry
+ endif
- exe 'sign place ' . (s:break_id + nr) . ' line=' . lnum . ' name=debugBreakpoint file=' . fnameescape(fname)
+ let lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
+ let entry['fname'] = fname
+ let entry['lnum'] = lnum
- let entry['fname'] = fname
- let entry['lnum'] = lnum
+ let bploc = printf('%s:%d', fname, lnum)
+ if !has_key(s:breakpoint_locations, bploc)
+ let s:breakpoint_locations[bploc] = []
+ endif
+ let s:breakpoint_locations[bploc] += [id]
+
+ if bufloaded(fname)
+ call s:PlaceSign(id, subid, entry)
+ endif
+ endfor
+endfunc
+
+func s:PlaceSign(id, subid, entry)
+ let nr = printf('%d.%d', a:id, a:subid)
+ exe 'sign place ' . s:Breakpoint2SignNumber(a:id, a:subid) . ' line=' . a:entry['lnum'] . ' name=debugBreakpoint' . nr . ' file=' . a:entry['fname']
+ let a:entry['placed'] = 1
endfunc
" Handle deleting a breakpoint
" Will remove the sign that shows the breakpoint
func s:HandleBreakpointDelete(msg)
- let nr = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
+ let id = substitute(a:msg, '.*id="\([0-9]*\)\".*', '\1', '') + 0
+ if empty(id)
+ return
+ endif
+ if has_key(s:breakpoints, id)
+ for [subid, entry] in items(s:breakpoints[id])
+ if has_key(entry, 'placed')
+ exe 'sign unplace ' . s:Breakpoint2SignNumber(id, subid)
+ unlet entry['placed']
+ endif
+ endfor
+ unlet s:breakpoints[id]
+ endif
+endfunc
+
+" Handle the debugged program starting to run.
+" Will store the process ID in s:pid
+func s:HandleProgramRun(msg)
+ let nr = substitute(a:msg, '.*pid="\([0-9]*\)\".*', '\1', '') + 0
if nr == 0
return
endif
- exe 'sign unplace ' . (s:break_id + nr)
- unlet s:breakpoints[nr]
+ let s:pid = nr
+ "call ch_log('Detected process ID: ' . s:pid)
+endfunc
+
+" Handle a BufRead autocommand event: place any signs.
+func s:BufRead()
+ let fname = expand('<afile>:p')
+ for [id, entries] in items(s:breakpoints)
+ for [subid, entry] in items(entries)
+ if entry['fname'] == fname
+ call s:PlaceSign(id, subid, entry)
+ endif
+ endfor
+ endfor
endfunc
+
+" Handle a BufUnloaded autocommand event: unplace any signs.
+func s:BufUnloaded()
+ let fname = expand('<afile>:p')
+ for [id, entries] in items(s:breakpoints)
+ for [subid, entry] in items(entries)
+ if entry['fname'] == fname
+ let entry['placed'] = 0
+ endif
+ endfor
+ endfor
+endfunc
+