0
0
mirror of https://github.com/vim/vim.git synced 2025-10-16 07:24:23 -04:00

runtime(termdebug): Add remote debugging capabilities

closes: #18429

Co-authored-by: Christian Brabandt <cb@256bit.org>
Signed-off-by: Miguel Barro <miguel.barro@live.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Miguel Barro
2025-10-08 18:15:51 +00:00
committed by Christian Brabandt
parent 143686b3c4
commit 3c5221f8ee
4 changed files with 475 additions and 52 deletions

View File

@@ -10952,8 +10952,12 @@ termdebug-example terminal.txt /*termdebug-example*
termdebug-frames terminal.txt /*termdebug-frames*
termdebug-mappings terminal.txt /*termdebug-mappings*
termdebug-prompt terminal.txt /*termdebug-prompt*
termdebug-remote terminal.txt /*termdebug-remote*
termdebug-remote-example terminal.txt /*termdebug-remote-example*
termdebug-remote-window terminal.txt /*termdebug-remote-window*
termdebug-starting terminal.txt /*termdebug-starting*
termdebug-stepping terminal.txt /*termdebug-stepping*
termdebug-substitute-path terminal.txt /*termdebug-substitute-path*
termdebug-timeout terminal.txt /*termdebug-timeout*
termdebug-variables terminal.txt /*termdebug-variables*
termdebug_contributing terminal.txt /*termdebug_contributing*

View File

@@ -1,4 +1,4 @@
*terminal.txt* For Vim version 9.1. Last change: 2025 Sep 15
*terminal.txt* For Vim version 9.1. Last change: 2025 Oct 08
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -44,6 +44,7 @@ If the result is "1" you have it.
Prompt mode |termdebug-prompt|
Mappings |termdebug-mappings|
Communication |termdebug-communication|
Remote Debugging |termdebug-remote|
Customizing |termdebug-customizing|
{only available when compiled with the |+terminal| feature}
@@ -1635,12 +1636,103 @@ interrupt the running program. But after using the MI command
communication channel.
Remote debugging ~
*termdebug-remote*
One of the main issues of remote debugging is the access to the debuggee's
source files. The plugin can profit from system and vim's networking
capabilities to workaround this.
*termdebug-remote-example*
The |termdebug-example| can be replicated by running the `gdb` debugger to
debug Vim on a remote Linux machine accessible via `ssh`.
- Build Vim as explained in the local example.
- If "socat" is not available in the remote machine 'terminal' mode will not
work properly. Fall back to |termdebug_use_prompt|: >
:let g:termdebug_config = {}
:let g:termdebug_config['use_prompt'] = v:true
- Specify the command line to run the remote `gdb` instance: >
:let g:termdebug_config['command'] = ['ssh', 'hostname', 'gdb']
< Explaining `ssh` is beyond the scope of this example, but notice the
command line can be greatly simplified by specifying the user, keys and
other options into the `$HOME/.ssh/config` file.
- Provide a hint for translating remote paths into |netrw| paths: >
:let g:termdebug_config['substitute_path'] = { '/': 'scp://hostname//' }
- Load the termdebug plugin and start debugging Vim: >
:packadd termdebug
:Termdebug vim
You now have the same three windows of the local example and can follow the
very same steps. The only difference is that the source windows displays a
netrw buffer instead of a local one.
*termdebug-substitute-path*
Use the `g:termdebug_config['substitute_path']` entry to map remote to local
files using the same strategy that gdb's `substitute-path` command uses.
For example:
- Use |netrw| to access files remoting via ssh: >
let g:termdebug_config['command'] = ['ssh', 'hostname', 'gdb']
let g:termdebug_config['substitute_path'] = { '/': 'scp://hostname//' }
< Note: that the key specifies the remote machine root path and the value
the local one.
- Use Windows' `UNC` paths to access `WSL2` sources: >
let g:termdebug_config['command'] = ['wsl', 'gdb']
let g:termdebug_config['substitute_path'] = {
\ '/': '\\wsl.localhost\Ubuntu-22.04\',
\ '/mnt/c/': 'C:/' }
< Note: that several mappings are required: one for each drive unit
and one for the linux filesystem (queried via `wslpath`).
In this mode any `ssh` or `wsl` command would be detected and a similar
command would be used to launch `socat` in a remote `tty` terminal session
and connect it to `gdb`.
If `socat` is not available a plain remote terminal would be used as
fallback.
The next session shows how to override this default behaviour.
*termdebug-remote-window*
In order to use another remote terminal client, set "remote_window" entry
in `g:termdebug_config` variable before invoking `:Termdebug`. For example:
- Debugging inside a docker container using "prompt" mode: >
let g:termdebug_config['use_prompt'] = v:true
let g:termdebug_config['command'] = ['docker', 'run', '-i',
\ '--rm', '--name', 'container-name', 'image-name', 'gdb']
let g:termdebug_config['remote_window'] =
\ ['docker', 'exec', '-ti', 'container-name'
\ ,'socat', '-dd', '-', 'PTY,raw,echo=0']
- Debugging inside a docker container using a "terminal buffer".
The container should be already running because unlike the previous
case for `terminal mode` "program" and "communication" ptys are created
before the gdb one: >
$ docker run -ti --rm --name container-name immage-name
< Then, launch the debugger: >
let g:termdebug_config['use_prompt'] = v:false " default
let g:termdebug_config['command'] =
\ ['docker', 'exec', '-ti', 'container-name', 'gdb']
let g:termdebug_config['remote_window'] =
\ ['docker', 'exec', '-ti', 'container-name'
\ ,'socat', '-dd', '-', 'PTY,raw,echo=0']
Note: "command" cannot use `-t` on |termdebug-prompt| mode because prompt
buffers cannot handle `tty` connections.
The "remote_window" command must use `-t` because otherwise it will lack
a `pty slave device` for gdb to connect.
Note: "socat" must be available in the remote machine on "terminal" mode.
Note: docker container sources can be accessible combining `volumes`
with mappings (see |termdebug-substitute-path|).
GDB command ~
*g:termdebugger*
To change the name of the gdb command, set "debugger" entry in
g:termdebug_config or the "g:termdebugger" variable before invoking
`:Termdebug`: >
let g:termdebug_config['command'] = "mygdb"
If there is no g:termdebug_config you can use: >
let g:termdebugger = "mygdb"
@@ -1648,6 +1740,7 @@ However, the latter form will be deprecated in future releases.
If the command needs an argument use a List: >
let g:termdebug_config['command'] = ['rr', 'replay', '--']
If there is no g:termdebug_config you can use: >
let g:termdebugger = ['rr', 'replay', '--']
@@ -1655,6 +1748,13 @@ Several arguments will be added to make gdb work well for the debugger.
If you want to modify them, add a function to filter the argument list: >
let g:termdebug_config['command_filter'] = MyDebugFilter
A "command_filter" scenario is solving escaping issues on remote debugging
over "ssh". For convenience a default filter is provided for escaping
whitespaces inside the arguments. It is automatically configured for "ssh",
but can be employed in other use cases like this: >
let g:termdebug_config['command_filter'] =
/ function('g:Termdebug_escape_whitespace')
If you do not want the arguments to be added, but you do need to set the
"pty", use a function to add the necessary arguments: >
let g:termdebug_config['command_add_args'] = MyAddArguments
@@ -1717,7 +1817,8 @@ than 99 will be displayed as "9+".
If you want to customize the breakpoint signs to show `>>` in the signcolumn: >
let g:termdebug_config['sign'] = '>>'
You can also specify individual signs for the first several breakpoints: >
let g:termdebug_config['signs'] = ['>1', '>2', '>3', '>4', '>5', '>6', '>7', '>8', '>9']
let g:termdebug_config['signs'] = ['>1', '>2', '>3', '>4', '>5',
\ '>6', '>7', '>8', '>9']
let g:termdebug_config['sign'] = '>>'
If you would like to use decimal (base 10) breakpoint signs: >
let g:termdebug_config['sign_decimal'] = 1

View File

@@ -4,7 +4,7 @@ vim9script
# Author: Bram Moolenaar
# Copyright: Vim license applies, see ":help license"
# Last Change: 2025 Sep 15
# Last Change: 2025 Oct 08
# Converted to Vim9: Ubaldo Tiberi <ubaldo.tiberi@gmail.com>
# WORK IN PROGRESS - The basics works stable, more to come
@@ -34,6 +34,7 @@ vim9script
# 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
# but a terminal window is used to run remote debugged programs.
# The communication with gdb uses GDB/MI. See:
# https://sourceware.org/gdb/current/onlinedocs/gdb/GDB_002fMI.html
@@ -435,8 +436,73 @@ def IsGdbStarted(): bool
return true
enddef
def CreateProgramPty(): string
ptybufnr = term_start('NONE', {
# Check if the debugger is running remotely and return a suitable command to pty remotely
def GetRemotePtyCmd(gdb_cmd: list<string>): list<string>
# Check if the user provided a command to launch the program window
var term_cmd = null_list
if exists('g:termdebug_config') && has_key(g:termdebug_config, 'remote_window')
term_cmd = g:termdebug_config['remote_window']
term_cmd = type(term_cmd) == v:t_list ? copy(term_cmd) : [term_cmd]
else
# Check if it is a remote gdb, the program terminal should be started
# on the remote machine.
const remote_pattern = '^\(ssh\|wsl\)'
if gdb_cmd[0] =~? remote_pattern
var gdb_pos = indexof(gdb_cmd, $'v:val =~? "^{GetCommand()[-1]}"')
if gdb_pos > 0
# strip debugger call
term_cmd = gdb_cmd[0 : gdb_pos - 1]
# roundtrip to check if socat is available on the remote side
silent call system(join(term_cmd, ' ') .. ' socat -h')
if v:shell_error
Echowarn('Install socat on the remote machine for a program window better experience')
else
# create a devoted tty slave device and link to stdin/stdout
term_cmd += ['socat', '-dd', '-', 'PTY,raw,echo=0']
ch_log($'launching remote ttys using "{join(term_cmd)}"')
endif
endif
endif
endif
return term_cmd
enddef
# Retrieve the remote pty device from a remote terminal
# If interact is true, use remote tty command to get the pty device
def GetRemotePtyDev(bufnr: number, interact: bool): string
var pty: string = null_string
var line = null_string
for j in range(5)
if interact
term_sendkeys(bufnr, "tty\<CR>")
endif
for i in range(0, term_getsize(bufnr)[0])
line = term_getline(bufnr, i)
if line =~? "/dev/pts"
pty = line
break
endif
term_wait(bufnr, 100)
endfor # i
if pty != null_string
# Clear the terminal window
if interact
term_sendkeys(bufnr, "clear\<CR>")
endif
break
endif
endfor # j
return pty
enddef
def CreateProgramPty(cmd: list<string> = null_list): string
ptybufnr = term_start(!cmd ? 'NONE' : cmd, {
term_name: ptybufname,
vertical: vvertical})
if ptybufnr == 0
@@ -454,20 +520,76 @@ def CreateProgramPty(): string
endif
endif
return job_info(term_getjob(ptybufnr))['tty_out']
if !cmd
return job_info(term_getjob(ptybufnr))['tty_out']
else
var interact = indexof(cmd, 'v:val =~? "^socat"') < 0
var pty = GetRemotePtyDev(ptybufnr, interact)
if pty !~? "/dev/pts"
Echoerr('Failed to get the program window tty')
exe $'bwipe! {ptybufnr}'
pty = null_string
elseif pty !~? "^/dev/pts"
# remove the prompt
pty = pty->matchstr('/dev/pts/\d\+')
endif
return pty
endif
enddef
def CreateCommunicationPty(): string
def CreateCommunicationPty(cmd: list<string> = null_list): string
# Create a hidden terminal window to communicate with gdb
commbufnr = term_start('NONE', {
term_name: commbufname,
out_cb: CommOutput,
hidden: 1
})
var options: dict<any> = { term_name: commbufname, out_cb: CommOutput, hidden: 1 }
if !cmd
commbufnr = term_start('NONE', options)
else
# avoid message wrapping that prevents proper parsing
options['term_cols'] = 500
commbufnr = term_start(cmd, options)
endif
if commbufnr == 0
return null_string
endif
return job_info(term_getjob(commbufnr))['tty_out']
if !cmd
return job_info(term_getjob(commbufnr))['tty_out']
else
# CommunicationPty only will be reliable with socat
if indexof(cmd, 'v:val =~? "^socat"') < 0
Echoerr('Communication window should be started with socat')
exe $'bwipe! {commbufnr}'
return null_string
endif
var pty = GetRemotePtyDev(commbufnr, false)
if pty !~? "/dev/pts"
Echoerr('Failed to get the communication window tty')
exe $'bwipe! {commbufnr}'
pty = null_string
elseif pty !~? "^/dev/pts"
# remove the prompt
pty = pty->matchstr('/dev/pts/\d\+')
endif
return pty
endif
enddef
# Convenient filter to workaround remote escaping issues.
# For example, ssh doesn't escape spaces for the gdb arguments.
# Workaround doing:
# let g:termdebug_config['command_filter'] = function('g:Termdebug_escape_whitespace')
def g:Termdebug_escape_whitespace(args: list<string>): list<string>
var new_args: list<string> = []
for arg in args
new_args += [substitute(arg, ' ', '\\ ', 'g')]
endfor
return new_args
enddef
def CreateGdbConsole(dict: dict<any>, pty: string, commpty: string): string
@@ -497,6 +619,12 @@ def CreateGdbConsole(dict: dict<any>, pty: string, commpty: string): string
gdb_cmd += ['-ex', 'echo startupdone\n']
endif
# Escape whitespaces in the gdb arguments for ssh remoting
if exists('g:termdebug_config') && !has_key(g:termdebug_config, 'command_filter') &&
gdb_cmd[0] =~? '^ssh'
g:termdebug_config['command_filter'] = function('g:Termdebug_escape_whitespace')
endif
if exists('g:termdebug_config') && has_key(g:termdebug_config, 'command_filter')
gdb_cmd = g:termdebug_config.command_filter(gdb_cmd)
endif
@@ -599,14 +727,18 @@ enddef
# Open a terminal window without a job, to run the debugged program in.
def StartDebug_term(dict: dict<any>)
var programpty = CreateProgramPty()
# Retrieve command if remote pty is needed
var gdb_cmd = GetCommand()
var term_cmd = GetRemotePtyCmd(gdb_cmd)
var programpty = CreateProgramPty(term_cmd)
if programpty is null_string
Echoerr('Failed to open the program terminal window')
CloseBuffers()
return
endif
var commpty = CreateCommunicationPty()
var commpty = CreateCommunicationPty(term_cmd)
if commpty is null_string
Echoerr('Failed to open the communication terminal window')
CloseBuffers()
@@ -656,16 +788,27 @@ def StartDebug_prompt(dict: dict<any>)
var gdb_args = get(dict, 'gdb_args', [])
var proc_args = get(dict, 'proc_args', [])
# Add -quiet to avoid the intro message causing a hit-enter prompt.
gdb_cmd += ['-quiet']
# directly communicate via mi2. This option must precede any -iex options for proper
# interpretation.
gdb_cmd += ['--interpreter=mi2']
# Disable pagination, it causes everything to stop at the gdb, needs to be run early
gdb_cmd += ['-iex', 'set pagination off']
# Interpret commands while the target is running. This should usually only
# be exec-interrupt, since many commands don't work properly while the
# target is running (so execute during startup).
gdb_cmd += ['-iex', 'set mi-async on']
# directly communicate via mi2
gdb_cmd += ['--interpreter=mi2']
# Add -quiet to avoid the intro message causing a hit-enter prompt.
gdb_cmd += ['-quiet']
# Escape whitespaces in the gdb arguments for ssh remoting
if exists('g:termdebug_config') && !has_key(g:termdebug_config, 'command_filter') &&
gdb_cmd[0] =~? '^ssh'
g:termdebug_config['command_filter'] = function('g:Termdebug_escape_whitespace')
endif
if exists('g:termdebug_config') && has_key(g:termdebug_config, 'command_filter')
gdb_cmd = g:termdebug_config.command_filter(gdb_cmd)
endif
# Adding arguments requested by the user
gdb_cmd += gdb_args
@@ -686,24 +829,61 @@ def StartDebug_prompt(dict: dict<any>)
set modified
gdb_channel = job_getchannel(gdbjob)
ptybufnr = 0
if has('win32')
# MS-Windows: run in a new console window for maximum compatibility
SendCommand('set new-console on')
elseif has('terminal')
# Unix: Run the debugged program in a terminal window. Open it below the
# gdb window.
belowright ptybufnr = term_start('NONE', {
term_name: 'debugged program',
vertical: vvertical
})
if ptybufnr == 0
Echoerr('Failed to open the program terminal window')
# Retrieve command if remote pty is needed
var term_cmd = GetRemotePtyCmd(gdb_cmd)
# If we are not using socat maybe is a shell:
var interact = indexof(term_cmd, 'v:val =~? "^socat"') < 0
if has('terminal') && (term_cmd != null || !has('win32'))
# Try open terminal twice because sync with gdbjob may not succeed
# the first time (docker daemon for example)
var trials: number = 2
var pty: string = null_string
while trials > 0
# Run the debugged program in a window. Open it below the
# gdb window.
belowright ptybufnr = term_start(
term_cmd != null ? term_cmd : 'NONE', {
term_name: 'debugged program',
vertical: vvertical
})
if ptybufnr == 0
Echoerr('Failed to open the program terminal window')
job_stop(gdbjob)
return
endif
ptywin = win_getid()
if term_cmd is null
pty = job_info(term_getjob(ptybufnr))['tty_out']
else
# Retrieve remote pty value
pty = GetRemotePtyDev(ptybufnr, interact)
endif
if pty !~? "/dev/pts"
exe $'bwipe! {ptybufnr}'
--trials
pty = null_string
else
break
endif
endwhile
if pty !~? "/dev/pts"
Echoerr('Failed to get the program windows tty')
job_stop(gdbjob)
return
elseif pty !~? "^/dev/pts"
# remove the prompt
pty = pty->matchstr('/dev/pts/\d\+')
endif
ptywin = win_getid()
var pty = job_info(term_getjob(ptybufnr))['tty_out']
SendCommand($'tty {pty}')
# Since GDB runs in a prompt window, the environment has not been set to
@@ -714,6 +894,9 @@ def StartDebug_prompt(dict: dict<any>)
SendCommand($'set env COLUMNS = {winwidth(ptywin)}')
SendCommand($'set env COLORS = {&t_Co}')
SendCommand($'set env VIM_TERMINAL = {v:version}')
elseif has('win32')
# MS-Windows: run in a new console window for maximum compatibility
SendCommand('set new-console on')
else
# TODO: open a new terminal, get the tty name, pass on to gdb
SendCommand('show inferior-tty')
@@ -930,7 +1113,7 @@ enddef
const NullRepl = 'XXXNULLXXX'
# Extract the "name" value from a gdb message with fullname="name".
def GetFullname(msg: string): string
def GetLocalFullname(msg: string): string
if msg !~ 'fullname'
return ''
endif
@@ -944,6 +1127,50 @@ def GetFullname(msg: string): string
return name
enddef
# Turn a remote machine local path into a remote one.
def Local2RemotePath(path: string): string
# If no mappings are provided keep the path.
if !exists('g:termdebug_config') || !has_key(g:termdebug_config, 'substitute_path')
return path
endif
var mappings: list<any> = items(g:termdebug_config['substitute_path'])
# Try to match the longest local path first.
sort(mappings, (a, b) => len(b[0]) - len(a[0]))
for [local, remote] in mappings
const pattern = '^' .. escape(local, '\.*~()')
if path =~ pattern
return substitute(path, pattern, escape(remote, '\.*~()'), '')
endif
endfor
return path
enddef
# Turn a remote path into a local one to the remote machine.
def Remote2LocalPath(path: string): string
# If no mappings are provided keep the path.
if !exists('g:termdebug_config') || !has_key(g:termdebug_config, 'substitute_path')
return path
endif
var mappings: list<any> = items(g:termdebug_config['substitute_path'])
# Try to match the longest remote path first.
sort(mappings, (a, b) => len(b[1]) - len(a[1]))
for [local, remote] in mappings
const pattern = '^' .. escape(substitute(remote, '[\/]', '[\\/]', 'g'), '.*~()')
if path =~ pattern
return substitute(path, pattern, local, '')
endif
endfor
return path
enddef
# Extract the "addr" value from a gdb message with addr="0x0001234".
def GetAsmAddr(msg: string): string
if msg !~ 'addr='
@@ -1159,7 +1386,7 @@ def CommOutput(chan: channel, message: string)
enddef
def GotoProgram()
if has('win32')
if has('win32') && !ptywin
if executable('powershell')
system(printf('powershell -Command "add-type -AssemblyName microsoft.VisualBasic;[Microsoft.VisualBasic.Interaction]::AppActivate(%d);"', pid))
endif
@@ -1374,7 +1601,8 @@ def Until(at: string)
ch_log('assume that program is running after this command')
# Use the fname:lnum format
var AT = empty(at) ? QuoteArg($"{expand('%:p')}:{line('.')}") : at
var fname = Remote2LocalPath(expand('%:p'))
var AT = empty(at) ? QuoteArg($"{fname}:{line('.')}") : at
SendCommand($'-exec-until {AT}')
else
ch_log('dropping command, program is running: exec-until')
@@ -1393,7 +1621,8 @@ def SetBreakpoint(at: string, tbreak=false)
endif
# Use the fname:lnum format, older gdb can't handle --source.
var AT = empty(at) ? QuoteArg($"{expand('%:p')}:{line('.')}") : at
var fname = Remote2LocalPath(expand('%:p'))
var AT = empty(at) ? QuoteArg($"{fname}:{line('.')}") : at
var cmd = ''
if tbreak
cmd = $'-break-insert -t {AT}'
@@ -1407,7 +1636,8 @@ def SetBreakpoint(at: string, tbreak=false)
enddef
def ClearBreakpoint()
var fname = fnameescape(expand('%:p'))
var fname = Remote2LocalPath(expand('%:p'))
fname = fnameescape(fname)
var lnum = line('.')
var bploc = printf('%s:%d', fname, lnum)
var nr = 0
@@ -1444,7 +1674,8 @@ def ClearBreakpoint()
enddef
def ToggleBreak()
var fname = fnameescape(expand('%:p'))
var fname = Remote2LocalPath(expand('%:p'))
fname = fnameescape(fname)
var lnum = line('.')
var bploc = printf('%s:%d', fname, lnum)
if has_key(breakpoint_locations, bploc)
@@ -1859,7 +2090,7 @@ def HandleCursor(msg: string)
var fname = ''
if msg =~ 'fullname='
fname = GetFullname(msg)
fname = GetLocalFullname(msg)
endif
if msg =~ 'addr='
@@ -1887,12 +2118,15 @@ def HandleCursor(msg: string)
SendCommand('-stack-list-variables 2')
endif
if msg =~ '^\(\*stopped\|=thread-selected\)' && filereadable(fname)
# Translate to remote file name if needed.
const fremote = Local2RemotePath(fname)
if msg =~ '^\(\*stopped\|=thread-selected\)' && (fremote != fname || filereadable(fname))
var lnum = substitute(msg, '.*line="\([^"]*\)".*', '\1', '')
if lnum =~ '^[0-9]*$'
GotoSourcewinOrCreateIt()
if expand('%:p') != fnamemodify(fname, ':p')
echomsg $"different fname: '{expand('%:p')}' vs '{fnamemodify(fname, ':p')}'"
if expand('%:p') != fnamemodify(fremote, ':p')
echomsg $"different fname: '{expand('%:p')}' vs '{fnamemodify(fremote, ':p')}'"
augroup Termdebug
# Always open a file read-only instead of showing the ATTENTION
# prompt, since it is unlikely we want to edit the file.
@@ -1904,11 +2138,11 @@ def HandleCursor(msg: string)
augroup END
if &modified
# TODO: find existing window
exe $'split {fnameescape(fname)}'
exe $'split {fnameescape(fremote)}'
sourcewin = win_getid()
InstallWinbar(false)
else
exe $'edit {fnameescape(fname)}'
exe $'edit {fnameescape(fremote)}'
endif
augroup Termdebug
au! SwapExists
@@ -1917,7 +2151,7 @@ def HandleCursor(msg: string)
exe $":{lnum}"
normal! zv
sign_unplace('TermDebug', {id: pc_id})
sign_place(pc_id, 'TermDebug', 'debugPC', fname,
sign_place(pc_id, 'TermDebug', 'debugPC', fremote,
{lnum: str2nr(lnum), priority: 110})
if !exists('b:save_signcolumn')
b:save_signcolumn = &signcolumn
@@ -1991,10 +2225,11 @@ def HandleNewBreakpoint(msg: string, modifiedFlag: bool)
endif
for mm in SplitMsg(msg)
var fname = GetFullname(mm)
var fname = GetLocalFullname(mm)
if empty(fname)
continue
endif
var fremote = Local2RemotePath(fname)
nr = substitute(mm, '.*number="\([0-9.]*\)\".*', '\1', '')
if empty(nr)
return
@@ -2034,11 +2269,11 @@ def HandleNewBreakpoint(msg: string, modifiedFlag: bool)
endif
var posMsg = ''
if bufloaded(fname)
if bufloaded(fremote)
PlaceSign(id, subid, entry)
posMsg = $' at line {lnum}.'
else
posMsg = $' in {fname} at line {lnum}.'
posMsg = $' in {fremote} at line {lnum}.'
endif
var actionTaken = ''
if !modifiedFlag
@@ -2055,8 +2290,9 @@ enddef
def PlaceSign(id: number, subid: number, entry: dict<any>)
var nr = printf('%d.%d', id, subid)
var remote = Local2RemotePath(entry['fname'])
sign_place(Breakpoint2SignNumber(id, subid), 'TermDebug',
$'debugBreakpoint{nr}', entry['fname'],
$'debugBreakpoint{nr}', remote,
{lnum: entry['lnum'], priority: 110})
entry['placed'] = 1
enddef

View File

@@ -689,4 +689,86 @@ func Test_termdebug_toggle_break()
%bw!
endfunc
" Check substitution capabilities and simulate remote debugging
func Test_termdebug_remote_basic()
let bin_name = 'XTD_basicremote'
let src_name = bin_name .. '.c'
call s:generate_files(bin_name)
defer s:cleanup_files(bin_name)
" Duplicate sources to test the mapping
const pwd = getcwd()
const src_shadow_dir = "shadow"
call mkdir(src_shadow_dir)
const src_shadow_file = $"{src_shadow_dir}/{src_name}"
call filecopy(src_name, src_shadow_file)
defer delete(src_shadow_dir, 'rf')
let modes = [v:true]
" termdebug only wokrs fine if socat is available on the remote machine
" otherwise the communication pty will be unstable
if executable('socat')
let modes += [v:false]
endif
for use_prompt in modes
" Set up mock remote and mapping
let g:termdebug_config = {}
let g:termdebug_config['use_prompt'] = use_prompt
" favor socat if available
if executable('socat')
let g:termdebug_config['remote_window'] =
\ ['socat', '-d', '-d', '-', 'PTY,raw,echo=0']
else
let g:termdebug_config['remote_window'] = ['sh']
endif
let g:termdebug_config['substitute_path'] = {}
let g:termdebug_config['substitute_path'][pwd] = pwd . '/' . src_shadow_dir
defer execute("unlet g:termdebug_config")
" Launch the debugger and set breakpoints in the shadow file instead
exe $"edit {src_shadow_file}"
exe $"Termdebug ./{bin_name}"
call WaitForAssert({-> assert_true(get(g:, "termdebug_is_running", v:false))})
call WaitForAssert({-> assert_equal(3, winnr('$'))})
let gdb_buf = winbufnr(1)
wincmd b
Break 9
sleep 100m
redraw!
call assert_equal([
\ {'lnum': 9, 'id': 1014, 'name': 'debugBreakpoint1.0',
\ 'priority': 110, 'group': 'TermDebug'}],
\ sign_getplaced('', #{group: 'TermDebug'})[0].signs)
Run
call term_wait(gdb_buf, 400)
redraw!
call WaitForAssert({-> assert_equal([
\ {'lnum': 9, 'id': 12, 'name': 'debugPC', 'priority': 110,
\ 'group': 'TermDebug'},
\ {'lnum': 9, 'id': 1014, 'name': 'debugBreakpoint1.0',
\ 'priority': 110, 'group': 'TermDebug'}],
\ sign_getplaced('', #{group: 'TermDebug'})[0].signs)})
Finish
call term_wait(gdb_buf)
redraw!
call WaitForAssert({-> assert_equal([
\ {'lnum': 9, 'id': 1014, 'name': 'debugBreakpoint1.0',
\ 'priority': 110, 'group': 'TermDebug'},
\ {'lnum': 20, 'id': 12, 'name': 'debugPC',
\ 'priority': 110, 'group': 'TermDebug'}],
\ sign_getplaced('', #{group: 'TermDebug'})[0].signs)})
" Cleanup, make sure the gdb job is terminated before return
" otherwise may interfere with next test
Gdb
bw!
call WaitForAssert({-> assert_equal(1, winnr('$'))})
endfor
%bw!
endfunc
" vim: shiftwidth=2 sts=2 expandtab