diff options
-rw-r--r-- | runtime/doc/nvim_terminal_emulator.txt | 35 | ||||
-rw-r--r-- | runtime/pack/dist/opt/termdebug/plugin/termdebug.vim | 611 |
2 files changed, 444 insertions, 202 deletions
diff --git a/runtime/doc/nvim_terminal_emulator.txt b/runtime/doc/nvim_terminal_emulator.txt index bbcc686207..d0d535566d 100644 --- a/runtime/doc/nvim_terminal_emulator.txt +++ b/runtime/doc/nvim_terminal_emulator.txt @@ -317,6 +317,18 @@ This is similar to using "print" in the gdb window. You can usually shorten `:Evaluate` to `:Ev`. +Navigating stack frames ~ + *termdebug-frames* *:Frame* *:Up* *:Down* + `:Frame` [frame] select frame [frame], which is a frame number, + address, or function name (default: current frame) + `:Up` [count] go up [count] frames (default: 1; the frame that + called the current) + `+` same (see |termdebug_map_plus| to disable) + `:Down` [count] go down [count] frames (default: 1; the frame called + by the current) + `-` same (see |termdebug_map_minus| to disable) + + Other commands ~ *termdebug-commands* *:Gdb* jump to the gdb window @@ -325,7 +337,9 @@ Other commands ~ isn't one *:Asm* jump to the window with the disassembly, create it if there isn't one - + *:Var* jump to the window with the local and argument variables, + create it if there isn't one. This window updates whenever the + program is stopped Events ~ *termdebug-events* @@ -386,11 +400,19 @@ If there is no g:termdebug_config you can use: >vim let g:termdebug_use_prompt = 1 < *termdebug_map_K* -The K key is normally mapped to :Evaluate. If you do not want this use: >vim +The K key is normally mapped to |:Evaluate|. If you do not want this use: >vim let g:termdebug_config['map_K'] = 0 If there is no g:termdebug_config you can use: >vim let g:termdebug_map_K = 0 < + *termdebug_map_minus* +The - key is normally mapped to |:Down|. If you do not want this use: >vim + let g:termdebug_config['map_minus'] = 0 +< + *termdebug_map_plus* +The + key is normally mapped to |:Up|. If you do not want this use: >vim + let g:termdebug_config['map_plus'] = 0 +< *termdebug_disasm_window* If you want the Asm window shown by default, set the "disasm_window" flag to 1. The "disasm_window_height" entry can be used to set the window height: >vim @@ -400,6 +422,15 @@ If there is no g:termdebug_config you can use: >vim let g:termdebug_disasm_window = 15 Any value greater than 1 will set the Asm window height to that value. + *termdebug_variables_window* +If you want the Var window shown by default, set the flag to 1. +the "variables_window_height" entry can be used to set the window height: >vim + let g:termdebug_config['variables_window'] = 1 + let g:termdebug_config['variables_window_height'] = 15 +If there is no g:termdebug_config you can use: >vim + let g:termdebug_variables_window = 15 +Any value greater than 1 will set the Var window height to that value. + Communication ~ *termdebug-communication* There is another, hidden, buffer, which is used for Vim to communicate with diff --git a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim index e8b78b3f5f..c436e015cc 100644 --- a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim +++ b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim @@ -2,7 +2,7 @@ " " Author: Bram Moolenaar " Copyright: Vim license applies, see ":help license" -" Last Change: 2023 Jun 24 +" Last Change: 2023 Aug 23 " " WORK IN PROGRESS - The basics works stable, more to come " Note: In general you need at least GDB 7.12 because this provides the @@ -75,6 +75,7 @@ let s:pc_id = 12 let s:asm_id = 13 let s:break_id = 14 " breakpoint number is added to this let s:stopped = 1 +let s:running = 0 let s:parsing_disasm_msg = 0 let s:asm_lines = [] @@ -127,6 +128,10 @@ func s:GetCommand() return type(cmd) == v:t_list ? copy(cmd) : [cmd] endfunc +func s:Echoerr(msg) + echohl ErrorMsg | echom '[termdebug] ' .. a:msg | echohl None +endfunc + 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}) @@ -139,18 +144,21 @@ endfunc func s:StartDebug_internal(dict) if exists('s:gdbwin') - echoerr 'Terminal debugger already running, cannot run two' + call s:Echoerr('Terminal debugger already running, cannot run two') return endif let gdbcmd = s:GetCommand() if !executable(gdbcmd[0]) - echoerr 'Cannot execute debugger program "' .. gdbcmd[0] .. '"' + call s:Echoerr('Cannot execute debugger program "' .. gdbcmd[0] .. '"') return endif let s:ptywin = 0 let s:pid = 0 let s:asmwin = 0 + let s:asmbuf = 0 + let s:varwin = 0 + let s:varbuf = 0 if exists('#User#TermdebugStartPre') doauto <nomodeline> User TermdebugStartPre @@ -159,7 +167,7 @@ func s:StartDebug_internal(dict) " Uncomment this line to write logging in "debuglog". " call ch_logfile('debuglog', 'w') - let s:sourcewin = win_getid(winnr()) + let s:sourcewin = win_getid() " Remember the old value of 'signcolumn' for each buffer that it's set in, so " that we can restore the value for all buffers. @@ -196,9 +204,9 @@ func s:StartDebug_internal(dict) endif if !has('win32') && !use_prompt let s:way = 'terminal' - else + else let s:way = 'prompt' - endif + endif if s:way == 'prompt' call s:StartDebug_prompt(a:dict) @@ -207,11 +215,17 @@ func s:StartDebug_internal(dict) endif if s:GetDisasmWindow() - let curwinid = win_getid(winnr()) + let curwinid = win_getid() call s:GotoAsmwinOrCreateIt() call win_gotoid(curwinid) endif + if s:GetVariablesWindow() + let curwinid = win_getid() + call s:GotoVariableswinOrCreateIt() + call win_gotoid(curwinid) + endif + if exists('#User#TermdebugStartPost') doauto <nomodeline> User TermdebugStartPost endif @@ -220,14 +234,21 @@ endfunc " Use when debugger didn't start or ended. func s:CloseBuffers() exe 'bwipe! ' . s:ptybuf + if s:asmbuf > 0 && bufexists(s:asmbuf) + exe 'bwipe! ' . s:asmbuf + endif + if s:varbuf > 0 && bufexists(s:varbuf) + exe 'bwipe! ' . s:varbuf + endif + let s:running = 0 unlet! s:gdbwin endfunc func s:CheckGdbRunning() - if !s:running - echoerr string(s:GetCommand()[0]) . ' exited unexpectedly' - call s:CloseBuffers() - return '' + if !s:gdb_running + call s:Echoerr(string(s:GetCommand()[0]) . ' exited unexpectedly') + call s:CloseBuffers() + return '' endif return 'ok' endfunc @@ -237,16 +258,16 @@ func s:StartDebug_term(dict) execute s:vertical ? 'vnew' : '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' + call s: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' + call s:Echoerr('Failed to open the program terminal window') return endif 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()) + let s:ptywin = win_getid() 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. @@ -264,11 +285,11 @@ func s:StartDebug_term(dict) \ }) " hide terminal buffer if s:comm_job_id == 0 - echoerr 'invalid argument (or job table is full) while opening communication terminal window' + call s: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' + call s:Echoerr('Failed to open the communication terminal window') exe 'bwipe! ' . s:ptybuf return endif @@ -309,19 +330,19 @@ func s:StartDebug_term(dict) " call ch_log('executing "' . join(gdb_cmd) . '"') let s:gdb_job_id = termopen(gdb_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' + call s:Echoerr('Invalid argument (or job table is full) while opening gdb terminal window') exe 'bwipe! ' . s:ptybuf return elseif s:gdb_job_id == -1 - echoerr 'Failed to open the gdb terminal window' + call s:Echoerr('Failed to open the gdb terminal window') call s:CloseBuffers() return endif - let s:running = v:true + let s:gdb_running = v:true let s:starting = v:true 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()) + let s:gdbwin = win_getid() " Wait for the "startupdone" message before sending any commands. let try_count = 0 @@ -369,7 +390,7 @@ func s:StartDebug_term(dict) " response can be in the same line or the next line let response = line1 . line2 if response =~ 'Undefined command' - echoerr 'Sorry, your gdb is too old, gdb 7.12 is required' + call s:Echoerr('Sorry, your gdb is too old, gdb 7.12 is required') " CHECKME: possibly send a "server show version" here call s:CloseBuffers() return @@ -388,7 +409,7 @@ func s:StartDebug_term(dict) endif let try_count += 1 if try_count > 100 - echoerr 'Cannot check if your gdb works, continuing anyway' + call s:Echoerr('Cannot check if your gdb works, continuing anyway') break endif sleep 10m @@ -409,7 +430,7 @@ func s:StartDebug_prompt(dict) else new endif - let s:gdbwin = win_getid(winnr()) + let s:gdbwin = win_getid() let s:promptbuf = bufnr('') call prompt_setprompt(s:promptbuf, 'gdb> ') set buftype=prompt @@ -443,18 +464,19 @@ func s:StartDebug_prompt(dict) " call ch_log('executing "' . join(gdb_cmd) . '"') let s:gdbjob = jobstart(gdb_cmd, { - \ 'on_exit': function('s:EndPromptDebug'), - \ 'on_stdout': function('s:JobOutCallback', {'last_line': '', 'real_cb': function('s:GdbOutCallback')}), - \ }) + \ 'on_exit': function('s:EndPromptDebug'), + \ 'on_stdout': function('s:JobOutCallback', {'last_line': '', 'real_cb': function('s:GdbOutCallback')}), + \ }) if s:gdbjob == 0 - echoerr 'invalid argument (or job table is full) while starting gdb job' + call s:Echoerr('Invalid argument (or job table is full) while starting gdb job') exe 'bwipe! ' . s:ptybuf return elseif s:gdbjob == -1 - echoerr 'Failed to start the gdb job' + call s:Echoerr('Failed to start the gdb job') call s:CloseBuffers() return endif + exe $'au BufUnload <buffer={s:promptbuf}> ++once call jobstop(s:gdbjob)' let s:ptybuf = 0 if has('win32') @@ -463,20 +485,19 @@ func s:StartDebug_prompt(dict) else " Unix: Run the debugged program in a terminal window. Open it below the " gdb window. - execute 'new' - wincmd x | wincmd j - belowright let s:pty_job_id = termopen('tail -f /dev/null;#gdb program') + belowright 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' + call s: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' + call s:Echoerr('Failed to open the program terminal window') return endif 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()) + let s:ptywin = win_getid() call s:SendCommand('tty ' . pty) " Since GDB runs in a prompt window, the environment has not been set to @@ -599,12 +620,12 @@ func s:PromptInterrupt() " Using job_stop() does not work on MS-Windows, need to send SIGTRAP to " the debugger program so that gdb responds again. if s:pid == 0 - echoerr 'Cannot interrupt gdb, did not find a process ID' + call s:Echoerr('Cannot interrupt gdb, did not find a process ID') else call debugbreak(s:pid) endif else - call jobstop(s:gdbjob) + call v:lua.vim.uv.kill(jobpid(s:gdbjob), 'sigint') endif endfunc @@ -629,33 +650,39 @@ endfunc func s:GdbOutCallback(job_id, msgs, event) "call ch_log('received from gdb: ' . a:text) - " Drop the gdb prompt, we have our own. - " Drop status and echo'd commands. - call filter(a:msgs, { index, val -> - \ val !=# '(gdb)' && val !=# '^done' && val[0] !=# '&'}) - + let comm_msgs = [] let lines = [] - let index = 0 for msg in a:msgs + " Disassembly messages need to be forwarded as-is. + if s:parsing_disasm_msg || msg =~ '^&"disassemble' + call s:CommOutput(a:job_id, [msg], a:event) + continue + endif + + " Drop the gdb prompt, we have our own. + " Drop status and echo'd commands. + if msg == '(gdb) ' || msg == '^done' || msg[0] == '&' + continue + endif + if msg =~ '^\^error,msg=' if exists('s:evalexpr') \ && s:DecodeMessage(msg[11:], v:false) \ =~ 'A syntax error in expression, near\|No symbol .* in current context' " Silently drop evaluation errors. - call remove(a:msgs, index) unlet s:evalexpr continue endif elseif msg[0] == '~' call add(lines, s:DecodeMessage(msg[1:], v:false)) - call remove(a:msgs, index) continue endif - let index += 1 + + call add(comm_msgs, msg) endfor - let curwinid = win_getid(winnr()) + let curwinid = win_getid() call win_gotoid(s:gdbwin) " Add the output above the current prompt. @@ -667,40 +694,42 @@ func s:GdbOutCallback(job_id, msgs, event) endif call win_gotoid(curwinid) - call s:CommOutput(a:job_id, a:msgs, a:event) + call s:CommOutput(a:job_id, comm_msgs, a:event) endfunc " Decode a message from gdb. "quotedText" starts with a ", return the text up -" to the next ", unescaping characters: +" to the next unescaped ", unescaping characters: " - remove line breaks (unless "literal" is v:true) +" - change \" to " " - change \\t to \t (unless "literal" is v:true) " - change \0xhh to \xhh (disabled for now) " - change \ooo to octal " - change \\ to \ func s:DecodeMessage(quotedText, literal) if a:quotedText[0] != '"' - echoerr 'DecodeMessage(): missing quote in ' . a:quotedText + call s:Echoerr('DecodeMessage(): missing quote in ' . a:quotedText) return endif let msg = a:quotedText - \ ->substitute('^"\|".*', '', 'g') - " multi-byte characters arrive in octal form - " NULL-values must be kept encoded as those break the string otherwise + \ ->substitute('^"\|[^\\]\zs".*', '', 'g') + \ ->substitute('\\"', '"', 'g') + "\ multi-byte characters arrive in octal form + "\ NULL-values must be kept encoded as those break the string otherwise \ ->substitute('\\000', s:NullRepl, 'g') \ ->substitute('\\\o\o\o', {-> eval('"' .. submatch(0) .. '"')}, 'g') - " Note: GDB docs also mention hex encodings - the translations below work - " but we keep them out for performance-reasons until we actually see - " those in mi-returns - " \ ->substitute('\\0x\(\x\x\)', {-> eval('"\x' .. submatch(1) .. '"')}, 'g') - " \ ->substitute('\\0x00', s:NullRepl, 'g') + "\ Note: GDB docs also mention hex encodings - the translations below work + "\ but we keep them out for performance-reasons until we actually see + "\ those in mi-returns + "\ \ ->substitute('\\0x\(\x\x\)', {-> eval('"\x' .. submatch(1) .. '"')}, 'g') + "\ \ ->substitute('\\0x00', s:NullRepl, 'g') \ ->substitute('\\\\', '\', 'g') \ ->substitute(s:NullRepl, '\\000', 'g') if !a:literal - return msg + return msg \ ->substitute('\\t', "\t", 'g') \ ->substitute('\\n', '', 'g') else - return msg + return msg endif endfunc const s:NullRepl = 'XXXNULLXXX' @@ -728,7 +757,7 @@ func s:GetAsmAddr(msg) endfunc func s:EndTermDebug(job_id, exit_code, event) - let s:running = v:false + let s:gdb_running = v:false if s:starting return endif @@ -738,16 +767,22 @@ func s:EndTermDebug(job_id, exit_code, event) endif unlet s:gdbwin - call s:EndDebugCommon() endfunc func s:EndDebugCommon() - let curwinid = win_getid(winnr()) + let curwinid = win_getid() if exists('s:ptybuf') && s:ptybuf exe 'bwipe! ' . s:ptybuf endif + if s:asmbuf > 0 && bufexists(s:asmbuf) + exe 'bwipe! ' . s:asmbuf + endif + if s:varbuf > 0 && bufexists(s:varbuf) + exe 'bwipe! ' . s:varbuf + endif + let s:running = 0 " Restore 'signcolumn' in all buffers for which it was set. call win_gotoid(s:sourcewin) @@ -785,11 +820,8 @@ func s:EndPromptDebug(job_id, exit_code, event) doauto <nomodeline> User TermdebugStopPre endif - let curwinid = win_getid(winnr()) - call win_gotoid(s:gdbwin) - close - if curwinid != s:gdbwin - call win_gotoid(curwinid) + if bufexists(s:promptbuf) + exe 'bwipe! ' . s:promptbuf endif call s:EndDebugCommon() @@ -797,7 +829,6 @@ func s:EndPromptDebug(job_id, exit_code, event) "call ch_log("Returning from EndPromptDebug()") endfunc -" - CommOutput: disassemble $pc " - CommOutput: &"disassemble $pc\n" " - CommOutput: ~"Dump of assembler code for function main(int, char**):\n" " - CommOutput: ~" 0x0000555556466f69 <+0>:\tpush rbp\n" @@ -807,15 +838,14 @@ endfunc " - CommOutput: ~"End of assembler dump.\n" " - CommOutput: ^done -" - CommOutput: disassemble $pc " - CommOutput: &"disassemble $pc\n" " - CommOutput: &"No function contains specified address.\n" " - CommOutput: ^error,msg="No function contains specified address." func s:HandleDisasmMsg(msg) if a:msg =~ '^\^done' - let curwinid = win_getid(winnr()) + let curwinid = win_getid() if win_gotoid(s:asmwin) - silent normal! gg0"_dG + silent! %delete _ call setline(1, s:asm_lines) set nomodified set filetype=asm @@ -839,15 +869,17 @@ func s:HandleDisasmMsg(msg) call s:SendCommand('disassemble $pc,+100') endif let s:parsing_disasm_msg = 0 - elseif a:msg =~ '\&\"disassemble \$pc' + elseif a:msg =~ '^&"disassemble \$pc' if a:msg =~ '+100' " This is our second disasm attempt let s:parsing_disasm_msg = 2 endif - else + elseif a:msg !~ '^&"disassemble' let value = substitute(a:msg, '^\~\"[ ]*', '', '') let value = substitute(value, '^=>[ ]*', '', '') - let value = substitute(value, '\\n\"\r$', '', '') + " Nvim already trims the final "\r" in s:CommOutput() + " let value = substitute(value, '\\n\"\r$', '', '') + let value = substitute(value, '\\n\"$', '', '') let value = substitute(value, '\r', '', '') let value = substitute(value, '\\t', ' ', 'g') @@ -857,12 +889,54 @@ func s:HandleDisasmMsg(msg) endif endfunc -func s:CommOutput(job_id, msgs, event) +func s:ParseVarinfo(varinfo) + let dict = {} + let nameIdx = matchstrpos(a:varinfo, '{name="\([^"]*\)"') + let dict['name'] = a:varinfo[nameIdx[1] + 7 : nameIdx[2] - 2] + let typeIdx = matchstrpos(a:varinfo, ',type="\([^"]*\)"') + let dict['type'] = a:varinfo[typeIdx[1] + 7 : typeIdx[2] - 2] + let valueIdx = matchstrpos(a:varinfo, ',value="\(.*\)"}') + if valueIdx[1] == -1 + let dict['value'] = 'Complex value' + else + let dict['value'] = a:varinfo[valueIdx[1] + 8 : valueIdx[2] - 3] + endif + return dict +endfunc +func s:HandleVariablesMsg(msg) + let curwinid = win_getid() + if win_gotoid(s:varwin) + + silent! %delete _ + let spaceBuffer = 20 + call setline(1, 'Type' . + \ repeat(' ', 16) . + \ 'Name' . + \ repeat(' ', 16) . + \ 'Value') + let cnt = 1 + let capture = '{name=".\{-}",\%(arg=".\{-}",\)\{0,1\}type=".\{-}"\%(,value=".\{-}"\)\{0,1\}}' + let varinfo = matchstr(a:msg, capture, 0, cnt) + while varinfo != '' + let vardict = s:ParseVarinfo(varinfo) + call setline(cnt + 1, vardict['type'] . + \ repeat(' ', max([20 - len(vardict['type']), 1])) . + \ vardict['name'] . + \ repeat(' ', max([20 - len(vardict['name']), 1])) . + \ vardict['value']) + let cnt += 1 + let varinfo = matchstr(a:msg, capture, 0, cnt) + endwhile + endif + call win_gotoid(curwinid) +endfunc + +func s:CommOutput(job_id, msgs, event) for msg in a:msgs - " remove prefixed NL - if msg[0] == "\n" - let msg = msg[1:] + " Nvim job lines are split on "\n", so trim a suffixed CR. + if msg[-1:] == "\r" + let msg = msg[:-2] endif if s:parsing_disasm_msg @@ -882,9 +956,12 @@ func s:CommOutput(job_id, msgs, event) call s:HandleEvaluate(msg) elseif msg =~ '^\^error,msg=' call s:HandleError(msg) - elseif msg =~ '^disassemble' + elseif msg =~ '^&"disassemble' let s:parsing_disasm_msg = 1 let s:asm_lines = [] + call s:HandleDisasmMsg(msg) + elseif msg =~ '^\^done,variables=' + call s:HandleVariablesMsg(msg) endif endif endfor @@ -925,11 +1002,16 @@ func s:InstallCommands() command Continue call chansend(s:gdb_job_id, "continue\r") endif + command -nargs=* Frame call s:Frame(<q-args>) + command -count=1 Up call s:Up(<count>) + command -count=1 Down call s:Down(<count>) + command -range -nargs=* Evaluate call s:Evaluate(<range>, <q-args>) command Gdb call win_gotoid(s:gdbwin) command Program call s:GotoProgram() command Source call s:GotoSourcewinOrCreateIt() command Asm call s:GotoAsmwinOrCreateIt() + command Var call s:GotoVariableswinOrCreateIt() command Winbar call s:InstallWinbar(1) let map = 1 @@ -939,17 +1021,29 @@ func s:InstallCommands() let map = g:termdebug_map_K endif if map - " let s:k_map_saved = maparg('K', 'n', 0, 1) - let s:k_map_saved = {} - for map in nvim_get_keymap('n') - if map.lhs ==# 'K' - let s:k_map_saved = map - break - endif - endfor + let s:k_map_saved = maparg('K', 'n', 0, 1) nnoremap K :Evaluate<CR> endif + let map = 1 + if exists('g:termdebug_config') + let map = get(g:termdebug_config, 'map_plus', 1) + endif + if map + let s:plus_map_saved = maparg('+', 'n', 0, 1) + nnoremap <expr> + $'<Cmd>{v:count1}Up<CR>' + endif + + let map = 1 + if exists('g:termdebug_config') + let map = get(g:termdebug_config, 'map_minus', 1) + endif + if map + let s:minus_map_saved = maparg('-', 'n', 0, 1) + nnoremap <expr> - $'<Cmd>{v:count1}Down<CR>' + endif + + if has('menu') && &mouse != '' call s:InstallWinbar(0) @@ -962,11 +1056,11 @@ func s:InstallCommands() if popup let s:saved_mousemodel = &mousemodel let &mousemodel = 'popup_setpos' - an 1.200 PopUp.-SEP3- <Nop> - an 1.210 PopUp.Set\ breakpoint :Break<CR> - an 1.220 PopUp.Clear\ breakpoint :Clear<CR> - an 1.230 PopUp.Run\ until :Until<CR> - an 1.240 PopUp.Evaluate :Evaluate<CR> + an 1.200 PopUp.-SEP3- <Nop> + an 1.210 PopUp.Set\ breakpoint :Break<CR> + an 1.220 PopUp.Clear\ breakpoint :Clear<CR> + an 1.230 PopUp.Run\ until :Until<CR> + an 1.240 PopUp.Evaluate :Evaluate<CR> endif endif @@ -984,7 +1078,7 @@ func s:InstallWinbar(force) " nnoremenu WinBar.Cont :Continue<CR> " nnoremenu WinBar.Stop :Stop<CR> " nnoremenu WinBar.Eval :Evaluate<CR> - " call add(s:winbar_winids, win_getid(winnr())) + " call add(s:winbar_winids, win_getid()) " endif endfunc @@ -1000,11 +1094,15 @@ func s:DeleteCommands() delcommand Arguments delcommand Stop delcommand Continue + delcommand Frame + delcommand Up + delcommand Down delcommand Evaluate delcommand Gdb delcommand Program delcommand Source delcommand Asm + delcommand Var delcommand Winbar if exists('s:k_map_saved') @@ -1016,10 +1114,28 @@ func s:DeleteCommands() endif unlet s:k_map_saved endif + if exists('s:plus_map_saved') + if empty(s:plus_map_saved) + nunmap + + else + " call mapset(s:plus_map_saved) + call mapset('n', 0, s:plus_map_saved) + endif + unlet s:plus_map_saved + endif + if exists('s:minus_map_saved') + if empty(s:minus_map_saved) + nunmap - + else + " call mapset(s:minus_map_saved) + call mapset('n', 0, s:minus_map_saved) + endif + unlet s:minus_map_saved + endif if has('menu') " Remove the WinBar entries from all windows where it was added. - " let curwinid = win_getid(winnr()) + " let curwinid = win_getid() " for winid in s:winbar_winids " if win_gotoid(winid) " aunmenu WinBar.Step @@ -1081,7 +1197,7 @@ func s:SetBreakpoint(at) " Use the fname:lnum format, older gdb can't handle --source. let at = empty(a:at) ? - \ fnameescape(expand('%:p')) . ':' . line('.') : a:at + \ fnameescape(expand('%:p')) . ':' . line('.') : a:at call s:SendCommand('-break-insert ' . at) if do_continue Continue @@ -1118,7 +1234,7 @@ func s:ClearBreakpoint() endif echomsg 'Breakpoint ' . id . ' cleared from line ' . lnum . '.' else - echoerr 'Internal error trying to remove breakpoint at line ' . lnum . '!' + call s:Echoerr('Internal error trying to remove breakpoint at line ' . lnum . '!') endif else echomsg 'No breakpoint to remove at line ' . lnum . '.' @@ -1132,6 +1248,37 @@ func s:Run(args) call s:SendResumingCommand('-exec-run') endfunc +" :Frame - go to a specfic frame in the stack +func s:Frame(arg) + " Note: we explicit do not use mi's command + " call s:SendCommand('-stack-select-frame "' . a:arg .'"') + " as we only get a "done" mi response and would have to open the file + " 'manually' - using cli command "frame" provides us with the mi response + " already parsed and allows for more formats + if a:arg =~ '^\d\+$' || a:arg == '' + " specify frame by number + call s:SendCommand('-interpreter-exec mi "frame ' . a:arg .'"') + elseif a:arg =~ '^0x[0-9a-fA-F]\+$' + " specify frame by stack address + call s:SendCommand('-interpreter-exec mi "frame address ' . a:arg .'"') + else + " specify frame by function name + call s:SendCommand('-interpreter-exec mi "frame function ' . a:arg .'"') + endif +endfunc + +" :Up - go a:count frames in the stack "higher" +func s:Up(count) + " the 'correct' one would be -stack-select-frame N, but we don't know N + call s:SendCommand($'-interpreter-exec console "up {a:count}"') +endfunc + +" :Down - go a:count frames in the stack "below" +func s:Down(count) + " the 'correct' one would be -stack-select-frame N, but we don't know N + call s:SendCommand($'-interpreter-exec console "down {a:count}"') +endfunc + func s:SendEval(expr) " check for "likely" boolean expressions, in which case we take it as lhs if a:expr =~ "[=!<>]=" @@ -1270,101 +1417,101 @@ function! s:CloseFloatingHoverOnCursorMove(win_id, opened) abort endfunction function! s:CloseFloatingHoverOnBufEnter(win_id, bufnr) abort - let winnr = win_id2win(a:win_id) - if winnr == 0 - " Float window was already closed - autocmd! nvim_termdebug_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 + let winnr = win_id2win(a:win_id) + if winnr == 0 + " Float window was already closed autocmd! nvim_termdebug_close_hover - call nvim_win_close(a:win_id, v:true) - endfunction + 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! nvim_termdebug_close_hover + call nvim_win_close(a:win_id, v:true) +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 pos = getpos('.') - - " Calculate width and height - let width = 0 - for index in range(len(lines)) - let line = lines[index] - let lw = strdisplaywidth(line) - if lw > width - let width = lw - endif - let lines[index] = line - endfor - - 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 + " 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 pos = getpos('.') + + " Calculate width and height + let width = 0 + for index in range(len(lines)) + let line = lines[index] + let lw = strdisplaywidth(line) + if lw > width + let width = lw endif + let lines[index] = line + endfor - " 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 height = len(lines) - let buf = nvim_create_buf(v:false, v:true) - call nvim_buf_set_lines(buf, 0, -1, v:true, lines) - " using v:true for second argument of nvim_open_win make the floating - " window disappear - let float_win_id = nvim_open_win(buf, v:false, { - \ 'relative': 'cursor', - \ 'anchor': vert . hor, - \ 'row': row, - \ 'col': col, - \ 'width': width, - \ 'height': height, - \ 'style': 'minimal', - \ }) - - if a:filetype isnot v:null - call nvim_set_option_value('filetype', a:filetype, { 'win' : float_win_id }) - endif + " 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 - call nvim_set_option_value('modified', v:false, { 'buf' : buf }) - call nvim_set_option_value('modifiable', v:false, { 'buf' : buf }) - - " 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 nvim_termdebug_close_hover - execute 'autocmd CursorMoved,CursorMovedI,InsertEnter <buffer> call ' . call_after_move - execute 'autocmd BufEnter * call ' . call_on_bufenter - augroup END + " Prefer West, but if there is no space, fallback into East + if pos[2] + width <= &columns + let hor = 'W' + let col = 0 else - echomsg a:lines[0] + let hor = 'E' + let col = 1 + endif + + let buf = nvim_create_buf(v:false, v:true) + call nvim_buf_set_lines(buf, 0, -1, v:true, lines) + " using v:true for second argument of nvim_open_win make the floating + " window disappear + let float_win_id = nvim_open_win(buf, v:false, { + \ 'relative': 'cursor', + \ 'anchor': vert . hor, + \ 'row': row, + \ 'col': col, + \ 'width': width, + \ 'height': height, + \ 'style': 'minimal', + \ }) + + if a:filetype isnot v:null + call nvim_set_option_value('filetype', a:filetype, { 'win' : float_win_id }) endif + + call nvim_set_option_value('modified', v:false, { 'buf' : buf }) + call nvim_set_option_value('modifiable', v:false, { 'buf' : buf }) + + " 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 nvim_termdebug_close_hover + execute 'autocmd CursorMoved,CursorMovedI,InsertEnter <buffer> call ' . call_after_move + execute 'autocmd BufEnter * call ' . call_on_bufenter + augroup END + else + echomsg a:lines[0] + endif endfunction " Handle an error. @@ -1376,13 +1523,13 @@ func s:HandleError(msg) return endif let msgVal = substitute(a:msg, '.*msg="\(.*\)"', '\1', '') - echoerr substitute(msgVal, '\\"', '"', 'g') + call s:Echoerr(substitute(msgVal, '\\"', '"', 'g')) endfunc func s:GotoSourcewinOrCreateIt() if !win_gotoid(s:sourcewin) new - let s:sourcewin = win_getid(winnr()) + let s:sourcewin = win_getid() call s:InstallWinbar(0) endif endfunc @@ -1415,19 +1562,21 @@ func s:GotoAsmwinOrCreateIt() exe 'new' endif - let s:asmwin = win_getid(winnr()) + let s:asmwin = win_getid() setlocal nowrap setlocal number setlocal noswapfile setlocal buftype=nofile + setlocal bufhidden=wipe + setlocal signcolumn=no setlocal modifiable - let asmbuf = bufnr('Termdebug-asm-listing') - if asmbuf > 0 - exe 'buffer' . asmbuf + if s:asmbuf > 0 && bufexists(s:asmbuf) + exe 'buffer' . s:asmbuf else - exe 'file Termdebug-asm-listing' + silent file Termdebug-asm-listing + let s:asmbuf = bufnr('Termdebug-asm-listing') endif if s:GetDisasmWindowHeight() > 0 @@ -1448,17 +1597,75 @@ func s:GotoAsmwinOrCreateIt() endif endfunc +func s:GetVariablesWindow() + if exists('g:termdebug_config') + return get(g:termdebug_config, 'variables_window', 0) + endif + if exists('g:termdebug_disasm_window') + return g:termdebug_variables_window + endif + return 0 +endfunc + +func s:GetVariablesWindowHeight() + if exists('g:termdebug_config') + return get(g:termdebug_config, 'variables_window_height', 0) + endif + if exists('g:termdebug_variables_window') && g:termdebug_variables_window > 1 + return g:termdebug_variables_window + endif + return 0 +endfunc + +func s:GotoVariableswinOrCreateIt() + if !win_gotoid(s:varwin) + if win_gotoid(s:sourcewin) + exe 'rightbelow new' + else + exe 'new' + endif + + let s:varwin = win_getid() + + setlocal nowrap + setlocal noswapfile + setlocal buftype=nofile + setlocal bufhidden=wipe + setlocal signcolumn=no + setlocal modifiable + + if s:varbuf > 0 && bufexists(s:varbuf) + exe 'buffer' . s:varbuf + else + silent file Termdebug-variables-listing + let s:varbuf = bufnr('Termdebug-variables-listing') + endif + + if s:GetVariablesWindowHeight() > 0 + exe 'resize ' .. s:GetVariablesWindowHeight() + endif + endif + + if s:running + call s:SendCommand('-stack-list-variables 2') + 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()) + let wid = win_getid() if a:msg =~ '^\*stopped' "call ch_log('program stopped') let s:stopped = 1 + if a:msg =~ '^\*stopped,reason="exited-normally"' + let s:running = 0 + endif elseif a:msg =~ '^\*running' "call ch_log('program running') let s:stopped = 0 + let s:running = 1 endif if a:msg =~ 'fullname=' @@ -1472,21 +1679,25 @@ func s:HandleCursor(msg) if asm_addr != '' let s:asm_addr = asm_addr - let curwinid = win_getid(winnr()) + let curwinid = win_getid() if win_gotoid(s:asmwin) - let lnum = search('^' . s:asm_addr) - if lnum == 0 - call s:SendCommand('disassemble $pc') - else - call sign_unplace('TermDebug', #{id: s:asm_id}) - call sign_place(s:asm_id, 'TermDebug', 'debugPC', '%', #{lnum: lnum}) - endif + let lnum = search('^' . s:asm_addr) + if lnum == 0 + call s:SendCommand('disassemble $pc') + else + call sign_unplace('TermDebug', #{id: s:asm_id}) + call sign_place(s:asm_id, 'TermDebug', 'debugPC', '%', #{lnum: lnum}) + endif - call win_gotoid(curwinid) + call win_gotoid(curwinid) endif endif endif + if s:running && s:stopped && bufwinnr('Termdebug-variables-listing') != -1 + call s:SendCommand('-stack-list-variables 2') + endif + if a:msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname) let lnum = substitute(a:msg, '.*line="\([^"]*\)".*', '\1', '') if lnum =~ '^[0-9]*$' @@ -1498,14 +1709,14 @@ func s:HandleCursor(msg) " prompt, since it is unlikely we want to edit the file. " The file may be changed but not saved, warn for that. au SwapExists * echohl WarningMsg - \ | echo 'Warning: file is being edited elsewhere' - \ | echohl None - \ | let v:swapchoice = 'o' + \ | echo 'Warning: file is being edited elsewhere' + \ | echohl None + \ | let v:swapchoice = 'o' augroup END if &modified " TODO: find existing window exe 'split ' . fnameescape(fname) - let s:sourcewin = win_getid(winnr()) + let s:sourcewin = win_getid() call s:InstallWinbar(0) else exe 'edit ' . fnameescape(fname) |