diff options
-rw-r--r-- | runtime/doc/nvim_terminal_emulator.txt | 253 | ||||
-rw-r--r-- | runtime/pack/dist/opt/termdebug/plugin/termdebug.vim | 827 |
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 + |