diff --git a/build/filters.ts b/build/filters.ts index 9a5766c7e62706..25ecb0bccdd2b3 100644 --- a/build/filters.ts +++ b/build/filters.ts @@ -188,6 +188,7 @@ export const copyrightFilter = Object.freeze([ '!**/*.sh', '!**/*.zsh', '!**/*.fish', + '!**/*.xsh', '!**/*.txt', '!**/*.xpm', '!**/*.opts', diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 178bed35e83c2f..4db5cda0b38788 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -274,6 +274,20 @@ export async function getShellIntegrationInjection( }); return { type, newArgs, envMixin, filesToCopy }; } + case 'xonsh': { + if (!originalArgs || originalArgs.length === 0) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Xonsh); + } else if (areXonshLoginArgs(originalArgs)) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.XonshLogin); + } + if (!newArgs) { + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedArgs }; + } + + newArgs = [...newArgs]; + newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + return { type, newArgs, envMixin }; + } } logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedShell }; @@ -325,6 +339,8 @@ enum ShellIntegrationExecutable { Bash = 'bash', Fish = 'fish', FishLogin = 'fish-login', + Xonsh = 'xonsh', + XonshLogin = 'xonsh-login', } const shellIntegrationArgs: Map = new Map(); @@ -338,10 +354,13 @@ shellIntegrationArgs.set(ShellIntegrationExecutable.ZshLogin, ['-il']); shellIntegrationArgs.set(ShellIntegrationExecutable.Bash, ['--init-file', '{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh']); shellIntegrationArgs.set(ShellIntegrationExecutable.Fish, ['--init-command', 'source "{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish"']); shellIntegrationArgs.set(ShellIntegrationExecutable.FishLogin, ['-l', '--init-command', 'source "{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish"']); +shellIntegrationArgs.set(ShellIntegrationExecutable.Xonsh, ['-i', '--rc', '{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-xonsh.xsh']); +shellIntegrationArgs.set(ShellIntegrationExecutable.XonshLogin, ['-l', '-i', '--rc', '{0}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-xonsh.xsh']); const pwshLoginArgs = ['-login', '-l']; const shLoginArgs = ['--login', '-l']; const shInteractiveArgs = ['-i', '--interactive']; const pwshImpliedArgs = ['-nol', '-nologo']; +const xonshLoginArgs = ['--login', '-l']; function arePwshLoginArgs(originalArgs: SingleOrMany): boolean { if (isString(originalArgs)) { @@ -370,6 +389,13 @@ function areZshBashFishLoginArgs(originalArgs: SingleOrMany): boolean { || !isString(originalArgs) && originalArgs.length === 1 && shLoginArgs.includes(originalArgs[0].toLowerCase()); } +function areXonshLoginArgs(originalArgs: SingleOrMany): boolean { + if (isString(originalArgs)) { + return xonshLoginArgs.includes(originalArgs.toLowerCase()); + } + return originalArgs.length === 1 && xonshLoginArgs.includes(originalArgs[0].toLowerCase()); +} + /** * Patterns that indicate sensitive environment variable names. */ diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index d8d166dc7fa169..1703064f5485b3 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -453,6 +453,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: GeneralShellType.Python }); } else if (sanitizedTitle.toLowerCase().startsWith('julia')) { this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: GeneralShellType.Julia }); + } else if (sanitizedTitle.toLowerCase().includes('xonsh')) { + this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: GeneralShellType.Xonsh }); } else { const shellTypeValue = posixShellTypeMap.get(sanitizedTitle) || generalShellTypeMap.get(sanitizedTitle); this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: shellTypeValue }); diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-xonsh.xsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-xonsh.xsh new file mode 100644 index 00000000000000..dcf60db93d4576 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-xonsh.xsh @@ -0,0 +1,123 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- +import sys +import os + +class _VSCodeXonshShellIntegration: + """vscode shell integration for xonsh shell.""" + + def __init__(self): + if 'VSCODE_INJECTION' in __xonsh__.env: + self._vscode_nonce = __xonsh__.env.get('VSCODE_NONCE', '') + self.stdout = sys.stdout + self.vsc_apply_xonshrc() + self.vsc_prompt_fields() + self.add_vsc_to_prompt() + self.add_vsc_events() + __xonsh__.env['VSCODE_SHELL_INTEGRATION'] = '1' + del __xonsh__.env['VSCODE_INJECTION'] + + def vsc_apply_xonshrc(self): + vscode_xonshrc = __xonsh__.env.get('XONSHRC', '') + if vscode_xonshrc: + for _vsc_rc_file in vscode_xonshrc: + if _vsc_rc_file and os.path.isfile(_vsc_rc_file): + try: + source @(_vsc_rc_file) + except Exception: + pass + + def _write_begin_osc(self): + self.stdout.write("\x1b]") + + def _write_end_osc(self): + self.stdout.write("\x07") + self.stdout.flush() + + def write_osc(self, msg): + self._write_begin_osc() + self.stdout.write(f"{msg}") + self._write_end_osc() + + def vsc_prompt_fields(self): + """Generate prompt with A marker before it""" + $PROMPT_FIELDS['vsc_prompt_start'] = "\001\x1b" + "]633;A" + "\x07\002" + $PROMPT_FIELDS['vsc_prompt_end'] = "\001\x1b" + "]633;B" + "\x07\002" + + + def add_vsc_to_prompt(self): + """Add vscode markers to prompt""" + if 'vsc_prompt_start' not in $PROMPT: + $PROMPT = '{vsc_prompt_start}' + $PROMPT + '{vsc_prompt_end}' + + + def _vsc_escape_value(self, value): + """ + Escape a value for use in OSC sequences. + Backslashes are doubled, semicolons and control chars are hex-escaped. + """ + if not value: + return '' + + result = [] + for char in value: + code = ord(char) + if char == '\n': + result.append(f'\\x{code:02x}') + rctrl_u = chr(21) + result.append(f'\\x{ord(rctrl_u):02x}') + elif code < 32: + result.append(f'\\x{code:02x}') + elif char == '\\': + result.append('\\\\') + elif char == ';': + result.append('\\x3b') + else: + result.append(char) + + return ''.join(result) + + def _vsc_command_executed(self, cmd): + """ + Send OSC 633 ; E and C - Command line and execution started + note: When sending E before C - vscode is overriding the sent command + """ + trimmed_cmd = cmd.strip() if cmd else '' + escaped_cmd = self._vsc_escape_value(trimmed_cmd) + self.write_osc(f'633;C') + self.write_osc(f'633;E;{escaped_cmd};{self._vscode_nonce}') + + def _vsc_command_finished(self, cmd, exit_code=None): + """Send OSC 633 ; D - Command finished with exit code""" + if not cmd or cmd.strip() == '' or exit_code is None: + # Empty command (just pressed Enter) + self.write_osc('633;D') + else: + self.write_osc(f'633;D;{exit_code}') + + def _vsc_cwd_updated(self, newdir): + self.write_osc(f'633;P;Cwd={self._vsc_escape_value(newdir)}') + + def add_vsc_events(self): + @events.on_precommand + def _vsc_on_precommand(cmd, **kwargs): + """Called before a command is executed""" + self._vsc_command_executed(cmd) + + @events.on_postcommand + def _vsc_on_postcommand(cmd, rtn, out, ts, **kwargs): + """Called after a command is executed""" + self._vsc_command_finished(cmd, rtn if rtn is not None else 0) + + @events.on_pre_prompt + def _vsc_on_pre_prompt(**kwargs): + self.add_vsc_to_prompt() + + @events.on_chdir + def _vsc_on_chdir(olddir, newdir): + self._vsc_cwd_updated(newdir) + +if 'VSCODE_SHELL_INTEGRATION' not in __xonsh__.env: + __xonsh__.vsc = _VSCodeXonshShellIntegration()