-*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
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}
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"
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', '--']
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
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
# 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
# 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
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
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
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
# 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()
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
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
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')
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
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='
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
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')
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}'
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
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)
var fname = ''
if msg =~ 'fullname='
- fname = GetFullname(msg)
+ fname = GetLocalFullname(msg)
endif
if msg =~ 'addr='
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.
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
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
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
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
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
%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