From bd52e53dbfa7641098313edd56d885df69916f9f Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Fri, 10 Mar 2023 13:55:17 +0100 Subject: [PATCH] Terminal: Add shell integration Change-Id: Ic1e226b56f0103e5a6e7764073ab7ab241b67baa Reviewed-by: Cristian Adam --- README.md | 30 ++- .../overview/creator-acknowledgements.qdoc | 13 + src/libs/utils/fileutils.cpp | 1 - src/libs/utils/fileutils.h | 1 - src/libs/utils/qtcprocess.cpp | 4 +- src/plugins/terminal/CMakeLists.txt | 1 + src/plugins/terminal/shellintegration.cpp | 143 ++++++++++ src/plugins/terminal/shellintegration.h | 34 +++ .../shellintegration-bash.sh | 252 ++++++++++++++++++ .../shellintegration-env.zsh | 15 ++ .../shellintegration-login.zsh | 7 + .../shellintegration-profile.zsh | 15 ++ .../shellintegrations/shellintegration-rc.zsh | 160 +++++++++++ .../shellintegrations/shellintegration.fish | 122 +++++++++ .../shellintegrations/shellintegration.ps1 | 158 +++++++++++ src/plugins/terminal/terminal.qrc | 15 +- src/plugins/terminal/terminalpane.cpp | 18 +- src/plugins/terminal/terminalsurface.cpp | 34 ++- src/plugins/terminal/terminalsurface.h | 5 +- src/plugins/terminal/terminalwidget.cpp | 37 ++- src/plugins/terminal/terminalwidget.h | 9 + src/plugins/terminal/tests/integration | 70 +++++ 22 files changed, 1129 insertions(+), 15 deletions(-) create mode 100644 src/plugins/terminal/shellintegration.cpp create mode 100644 src/plugins/terminal/shellintegration.h create mode 100755 src/plugins/terminal/shellintegrations/shellintegration-bash.sh create mode 100644 src/plugins/terminal/shellintegrations/shellintegration-env.zsh create mode 100644 src/plugins/terminal/shellintegrations/shellintegration-login.zsh create mode 100644 src/plugins/terminal/shellintegrations/shellintegration-profile.zsh create mode 100644 src/plugins/terminal/shellintegrations/shellintegration-rc.zsh create mode 100644 src/plugins/terminal/shellintegrations/shellintegration.fish create mode 100644 src/plugins/terminal/shellintegrations/shellintegration.ps1 create mode 100755 src/plugins/terminal/tests/integration diff --git a/README.md b/README.md index c58acc13f83..1866b96052f 100644 --- a/README.md +++ b/README.md @@ -783,7 +783,6 @@ SQLite (https://www.sqlite.org) is in the Public Domain. ### libvterm - An abstract C99 library which implements a VT220 or xterm-like terminal emulator. It doesn't use any particular graphics toolkit or output system, instead it invokes callback function pointers that its embedding program should provide it to draw on its behalf. @@ -813,3 +812,32 @@ SQLite (https://www.sqlite.org) is in the Public Domain. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +### terminal/shellintegrations + + The Terminal plugin uses scripts to integrate with the shell. The scripts are + located in the Qt Creator source tree in src/plugins/terminal/shellintegrations. + + https://github.com/microsoft/vscode/tree/main/src/vs/workbench/contrib/terminal/browser/media + + MIT License + + Copyright (c) 2015 - present Microsoft Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/doc/qtcreator/src/overview/creator-acknowledgements.qdoc b/doc/qtcreator/src/overview/creator-acknowledgements.qdoc index 29b006985d1..820e97247d5 100644 --- a/doc/qtcreator/src/overview/creator-acknowledgements.qdoc +++ b/doc/qtcreator/src/overview/creator-acknowledgements.qdoc @@ -1046,5 +1046,18 @@ \include license-mit.qdocinc + \li \b terminal/shellintegrations + + The Terminal plugin uses scripts to integrate with the shell. The scripts are + located in the Qt Creator source tree in src/plugins/terminal/shellintegrations. + + \list + \li \l https://github.com/microsoft/vscode/tree/main/src/vs/workbench/contrib/terminal/browser/media + \endlist + + Distributed under the MIT license. + + \include license-mit.qdocinc + \endlist */ diff --git a/src/libs/utils/fileutils.cpp b/src/libs/utils/fileutils.cpp index c0a574f9c34..4bd0692e133 100644 --- a/src/libs/utils/fileutils.cpp +++ b/src/libs/utils/fileutils.cpp @@ -5,7 +5,6 @@ #include "savefile.h" #include "algorithm.h" -#include "hostosinfo.h" #include "qtcassert.h" #include "utilstr.h" diff --git a/src/libs/utils/fileutils.h b/src/libs/utils/fileutils.h index 19f17a6d719..a1a7ffef977 100644 --- a/src/libs/utils/fileutils.h +++ b/src/libs/utils/fileutils.h @@ -121,7 +121,6 @@ public: QString *selectedFilter = nullptr, QFileDialog::Options options = {}); #endif - }; // for actually finding out if e.g. directories are writable on Windows diff --git a/src/libs/utils/qtcprocess.cpp b/src/libs/utils/qtcprocess.cpp index a5de95d29db..21b1684242b 100644 --- a/src/libs/utils/qtcprocess.cpp +++ b/src/libs/utils/qtcprocess.cpp @@ -355,7 +355,9 @@ public: bool startResult = m_ptyProcess->startProcess(program, - arguments, + HostOsInfo::isWindowsHost() + ? QStringList{m_setup.m_nativeArguments} << arguments + : arguments, m_setup.m_workingDirectory.path(), m_setup.m_environment.toProcessEnvironment().toStringList(), m_setup.m_ptyData->size().width(), diff --git a/src/plugins/terminal/CMakeLists.txt b/src/plugins/terminal/CMakeLists.txt index c0972eccf36..f68a0b2ce42 100644 --- a/src/plugins/terminal/CMakeLists.txt +++ b/src/plugins/terminal/CMakeLists.txt @@ -7,6 +7,7 @@ add_qtc_plugin(Terminal glyphcache.cpp glyphcache.h keys.cpp keys.h scrollback.cpp scrollback.h + shellintegration.cpp shellintegration.h shellmodel.cpp shellmodel.h terminal.qrc terminalpane.cpp terminalpane.h diff --git a/src/plugins/terminal/shellintegration.cpp b/src/plugins/terminal/shellintegration.cpp new file mode 100644 index 00000000000..fd4c1364696 --- /dev/null +++ b/src/plugins/terminal/shellintegration.cpp @@ -0,0 +1,143 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "shellintegration.h" + +#include +#include +#include + +#include + +Q_LOGGING_CATEGORY(integrationLog, "qtc.terminal.shellintegration", QtWarningMsg) + +using namespace Utils; + +namespace Terminal { + +struct FileToCopy +{ + FilePath source; + QString destName; +}; + +// clang-format off +struct +{ + struct + { + FilePath rcFile{":/terminal/shellintegrations/shellintegration-bash.sh"}; + } bash; + struct + { + QList files{ + {":/terminal/shellintegrations/shellintegration-env.zsh", ".zshenv"}, + {":/terminal/shellintegrations/shellintegration-login.zsh", ".zlogin"}, + {":/terminal/shellintegrations/shellintegration-profile.zsh", ".zprofile"}, + {":/terminal/shellintegrations/shellintegration-rc.zsh", ".zshrc"} + }; + } zsh; + struct + { + FilePath script{":/terminal/shellintegrations/shellintegration.ps1"}; + } pwsh; + +} filesToCopy; +// clang-format on + +bool ShellIntegration::canIntegrate(const Utils::CommandLine &cmdLine) +{ + if (cmdLine.executable().needsDevice()) + return false; // TODO: Allow integration for remote shells + + if (!cmdLine.arguments().isEmpty()) + return false; + + if (cmdLine.executable().baseName() == "bash") + return true; + + if (cmdLine.executable().baseName() == "zsh") + return true; + + if (cmdLine.executable().baseName() == "pwsh" + || cmdLine.executable().baseName() == "powershell") { + return true; + } + + return false; +} + +void ShellIntegration::onOsc(int cmd, const VTermStringFragment &fragment) +{ + QString d = QString::fromLocal8Bit(fragment.str, fragment.len); + const auto [command, data] = Utils::splitAtFirst(d, ';'); + + if (cmd == 1337) { + const auto [key, value] = Utils::splitAtFirst(command, '='); + if (key == QStringView(u"CurrentDir")) + emit currentDirChanged(FilePath::fromUserInput(value.toString()).path()); + + } else if (cmd == 7) { + emit currentDirChanged(FilePath::fromUserInput(d).path()); + } else if (cmd == 133) { + qCDebug(integrationLog) << "OSC 133:" << data; + } else if (cmd == 633 && command.length() == 1) { + if (command[0] == 'E') { + CommandLine cmdLine = CommandLine::fromUserInput(data.toString()); + emit commandChanged(cmdLine); + } else if (command[0] == 'D') { + emit commandChanged({}); + } else if (command[0] == 'P') { + const auto [key, value] = Utils::splitAtFirst(data, '='); + if (key == QStringView(u"Cwd")) + emit currentDirChanged(value.toString()); + } + } +} + +void ShellIntegration::prepareProcess(Utils::QtcProcess &process) +{ + Environment env = process.environment().hasChanges() ? process.environment() + : Environment::systemEnvironment(); + CommandLine cmd = process.commandLine(); + + if (!canIntegrate(cmd)) + return; + + env.set("VSCODE_INJECTION", "1"); + + if (cmd.executable().baseName() == "bash") { + const FilePath rcPath = filesToCopy.bash.rcFile; + const FilePath tmpRc = FilePath::fromUserInput( + m_tempDir.filePath(filesToCopy.bash.rcFile.fileName())); + rcPath.copyFile(tmpRc); + + cmd.addArgs({"--init-file", tmpRc.nativePath()}); + } else if (cmd.executable().baseName() == "zsh") { + for (const FileToCopy &file : filesToCopy.zsh.files) { + const auto copyResult = file.source.copyFile( + FilePath::fromUserInput(m_tempDir.filePath(file.destName))); + QTC_ASSERT_EXPECTED(copyResult, return); + } + + const Utils::FilePath originalZdotDir = FilePath::fromUserInput( + env.value_or("ZDOTDIR", QDir::homePath())); + + env.set("ZDOTDIR", m_tempDir.path()); + env.set("USER_ZDOTDIR", originalZdotDir.nativePath()); + } else if (cmd.executable().baseName() == "pwsh" + || cmd.executable().baseName() == "powershell") { + const FilePath rcPath = filesToCopy.pwsh.script; + const FilePath tmpRc = FilePath::fromUserInput( + m_tempDir.filePath(filesToCopy.pwsh.script.fileName())); + rcPath.copyFile(tmpRc); + + cmd.addArgs(QString("-noexit -command try { . \"%1\" } catch {}{1}").arg(tmpRc.nativePath()), + CommandLine::Raw); + } + + process.setCommand(cmd); + process.setEnvironment(env); +} + +} // namespace Terminal diff --git a/src/plugins/terminal/shellintegration.h b/src/plugins/terminal/shellintegration.h new file mode 100644 index 00000000000..264b5a4d67a --- /dev/null +++ b/src/plugins/terminal/shellintegration.h @@ -0,0 +1,34 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH +// Qt-GPL-exception-1.0 + +#pragma once + +#include +#include + +#include + +#include + +namespace Terminal { + +class ShellIntegration : public QObject +{ + Q_OBJECT +public: + static bool canIntegrate(const Utils::CommandLine &cmdLine); + + void onOsc(int cmd, const VTermStringFragment &fragment); + + void prepareProcess(Utils::QtcProcess &process); + +signals: + void commandChanged(const Utils::CommandLine &command); + void currentDirChanged(const QString &dir); + +private: + QTemporaryDir m_tempDir; +}; + +} // namespace Terminal diff --git a/src/plugins/terminal/shellintegrations/shellintegration-bash.sh b/src/plugins/terminal/shellintegrations/shellintegration-bash.sh new file mode 100755 index 00000000000..7db188be08e --- /dev/null +++ b/src/plugins/terminal/shellintegrations/shellintegration-bash.sh @@ -0,0 +1,252 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + + +# Prevent the script recursing when setting up +if [[ -n "$VSCODE_SHELL_INTEGRATION" ]]; then + builtin return +fi + +VSCODE_SHELL_INTEGRATION=1 + +# Run relevant rc/profile only if shell integration has been injected, not when run manually +if [ "$VSCODE_INJECTION" == "1" ]; then + if [ -z "$VSCODE_SHELL_LOGIN" ]; then + if [ -r ~/.bashrc ]; then + . ~/.bashrc + fi + else + # Imitate -l because --init-file doesn't support it: + # run the first of these files that exists + if [ -r /etc/profile ]; then + . /etc/profile + fi + # exceute the first that exists + if [ -r ~/.bash_profile ]; then + . ~/.bash_profile + elif [ -r ~/.bash_login ]; then + . ~/.bash_login + elif [ -r ~/.profile ]; then + . ~/.profile + fi + builtin unset VSCODE_SHELL_LOGIN + + # Apply any explicit path prefix (see #99878) + if [ -n "$VSCODE_PATH_PREFIX" ]; then + export PATH=$VSCODE_PATH_PREFIX$PATH + builtin unset VSCODE_PATH_PREFIX + fi + fi + builtin unset VSCODE_INJECTION +fi + +if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then + builtin return +fi + +__vsc_get_trap() { + # 'trap -p DEBUG' outputs a shell command like `trap -- '…shellcode…' DEBUG`. + # The terms are quoted literals, but are not guaranteed to be on a single line. + # (Consider a trap like $'echo foo\necho \'bar\''). + # To parse, we splice those terms into an expression capturing them into an array. + # This preserves the quoting of those terms: when we `eval` that expression, they are preserved exactly. + # This is different than simply exploding the string, which would split everything on IFS, oblivious to quoting. + builtin local -a terms + builtin eval "terms=( $(trap -p "${1:-DEBUG}") )" + # |________________________| + # | + # \-------------------*--------------------/ + # terms=( trap -- '…arbitrary shellcode…' DEBUG ) + # |____||__| |_____________________| |_____| + # | | | | + # 0 1 2 3 + # | + # \--------*----/ + builtin printf '%s' "${terms[2]:-}" +} + +# The property (P) and command (E) codes embed values which require escaping. +# Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex. +__vsc_escape_value() { + # Process text byte by byte, not by codepoint. + builtin local LC_ALL=C str="${1}" i byte token out='' + + for (( i=0; i < "${#str}"; ++i )); do + byte="${str:$i:1}" + + # Escape backslashes and semi-colons + if [ "$byte" = "\\" ]; then + token="\\\\" + elif [ "$byte" = ";" ]; then + token="\\x3b" + else + token="$byte" + fi + + out+="$token" + done + + builtin printf '%s\n' "${out}" +} + +# Send the IsWindows property if the environment looks like Windows +if [[ "$(uname -s)" =~ ^CYGWIN*|MINGW*|MSYS* ]]; then + builtin printf '\e]633;P;IsWindows=True\a' +fi + +# Allow verifying $BASH_COMMAND doesn't have aliases resolved via history when the right HISTCONTROL +# configuration is used +if [[ "$HISTCONTROL" =~ .*(erasedups|ignoreboth|ignoredups).* ]]; then + __vsc_history_verify=0 +else + __vsc_history_verify=1 +fi + +__vsc_initialized=0 +__vsc_original_PS1="$PS1" +__vsc_original_PS2="$PS2" +__vsc_custom_PS1="" +__vsc_custom_PS2="" +__vsc_in_command_execution="1" +__vsc_current_command="" + +__vsc_prompt_start() { + builtin printf '\e]633;A\a' +} + +__vsc_prompt_end() { + builtin printf '\e]633;B\a' +} + +__vsc_update_cwd() { + builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "$PWD")" +} + +__vsc_command_output_start() { + builtin printf '\e]633;C\a' + builtin printf '\e]633;E;%s\a' "$(__vsc_escape_value "${__vsc_current_command}")" +} + +__vsc_continuation_start() { + builtin printf '\e]633;F\a' +} + +__vsc_continuation_end() { + builtin printf '\e]633;G\a' +} + +__vsc_command_complete() { + if [ "$__vsc_current_command" = "" ]; then + builtin printf '\e]633;D\a' + else + builtin printf '\e]633;D;%s\a' "$__vsc_status" + fi + __vsc_update_cwd +} +__vsc_update_prompt() { + # in command execution + if [ "$__vsc_in_command_execution" = "1" ]; then + # Wrap the prompt if it is not yet wrapped, if the PS1 changed this this was last set it + # means the user re-exported the PS1 so we should re-wrap it + if [[ "$__vsc_custom_PS1" == "" || "$__vsc_custom_PS1" != "$PS1" ]]; then + __vsc_original_PS1=$PS1 + __vsc_custom_PS1="\[$(__vsc_prompt_start)\]$__vsc_original_PS1\[$(__vsc_prompt_end)\]" + PS1="$__vsc_custom_PS1" + fi + if [[ "$__vsc_custom_PS2" == "" || "$__vsc_custom_PS2" != "$PS2" ]]; then + __vsc_original_PS2=$PS2 + __vsc_custom_PS2="\[$(__vsc_continuation_start)\]$__vsc_original_PS2\[$(__vsc_continuation_end)\]" + PS2="$__vsc_custom_PS2" + fi + __vsc_in_command_execution="0" + fi +} + +__vsc_precmd() { + __vsc_command_complete "$__vsc_status" + __vsc_current_command="" + __vsc_update_prompt +} + +__vsc_preexec() { + __vsc_initialized=1 + if [[ ! "$BASH_COMMAND" =~ ^__vsc_prompt* ]]; then + # Use history if it's available to verify the command as BASH_COMMAND comes in with aliases + # resolved + if [ "$__vsc_history_verify" = "1" ]; then + __vsc_current_command="$(builtin history 1 | sed 's/ *[0-9]* *//')" + else + __vsc_current_command=$BASH_COMMAND + fi + else + __vsc_current_command="" + fi + __vsc_command_output_start +} + +# Debug trapping/preexec inspired by starship (ISC) +if [[ -n "${bash_preexec_imported:-}" ]]; then + __vsc_preexec_only() { + if [ "$__vsc_in_command_execution" = "0" ]; then + __vsc_in_command_execution="1" + __vsc_preexec + fi + } + precmd_functions+=(__vsc_prompt_cmd) + preexec_functions+=(__vsc_preexec_only) +else + __vsc_dbg_trap="$(__vsc_get_trap DEBUG)" + + if [[ -z "$__vsc_dbg_trap" ]]; then + __vsc_preexec_only() { + if [ "$__vsc_in_command_execution" = "0" ]; then + __vsc_in_command_execution="1" + __vsc_preexec + fi + } + trap '__vsc_preexec_only "$_"' DEBUG + elif [[ "$__vsc_dbg_trap" != '__vsc_preexec "$_"' && "$__vsc_dbg_trap" != '__vsc_preexec_all "$_"' ]]; then + __vsc_preexec_all() { + if [ "$__vsc_in_command_execution" = "0" ]; then + __vsc_in_command_execution="1" + builtin eval "${__vsc_dbg_trap}" + __vsc_preexec + fi + } + trap '__vsc_preexec_all "$_"' DEBUG + fi +fi + +__vsc_update_prompt + +__vsc_restore_exit_code() { + return "$1" +} + +__vsc_prompt_cmd_original() { + __vsc_status="$?" + __vsc_restore_exit_code "${__vsc_status}" + # Evaluate the original PROMPT_COMMAND similarly to how bash would normally + # See https://unix.stackexchange.com/a/672843 for technique + for cmd in "${__vsc_original_prompt_command[@]}"; do + eval "${cmd:-}" + done + __vsc_precmd +} + +__vsc_prompt_cmd() { + __vsc_status="$?" + __vsc_precmd +} + +# PROMPT_COMMAND arrays and strings seem to be handled the same (handling only the first entry of +# the array?) +__vsc_original_prompt_command=$PROMPT_COMMAND + +if [[ -z "${bash_preexec_imported:-}" ]]; then + if [[ -n "$__vsc_original_prompt_command" && "$__vsc_original_prompt_command" != "__vsc_prompt_cmd" ]]; then + PROMPT_COMMAND=__vsc_prompt_cmd_original + else + PROMPT_COMMAND=__vsc_prompt_cmd + fi +fi diff --git a/src/plugins/terminal/shellintegrations/shellintegration-env.zsh b/src/plugins/terminal/shellintegrations/shellintegration-env.zsh new file mode 100644 index 00000000000..3c890539aeb --- /dev/null +++ b/src/plugins/terminal/shellintegrations/shellintegration-env.zsh @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +if [[ -f $USER_ZDOTDIR/.zshenv ]]; then + VSCODE_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$USER_ZDOTDIR + + # prevent recursion + if [[ $USER_ZDOTDIR != $VSCODE_ZDOTDIR ]]; then + . $USER_ZDOTDIR/.zshenv + fi + + USER_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$VSCODE_ZDOTDIR +fi diff --git a/src/plugins/terminal/shellintegrations/shellintegration-login.zsh b/src/plugins/terminal/shellintegrations/shellintegration-login.zsh new file mode 100644 index 00000000000..37ff5439790 --- /dev/null +++ b/src/plugins/terminal/shellintegrations/shellintegration-login.zsh @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +ZDOTDIR=$USER_ZDOTDIR +if [[ $options[norcs] = off && -o "login" && -f $ZDOTDIR/.zlogin ]]; then + . $ZDOTDIR/.zlogin +fi diff --git a/src/plugins/terminal/shellintegrations/shellintegration-profile.zsh b/src/plugins/terminal/shellintegrations/shellintegration-profile.zsh new file mode 100644 index 00000000000..724e1f28790 --- /dev/null +++ b/src/plugins/terminal/shellintegrations/shellintegration-profile.zsh @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +if [[ $options[norcs] = off && -o "login" && -f $USER_ZDOTDIR/.zprofile ]]; then + VSCODE_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$USER_ZDOTDIR + . $USER_ZDOTDIR/.zprofile + ZDOTDIR=$VSCODE_ZDOTDIR + + # Apply any explicit path prefix (see #99878) + if (( ${+VSCODE_PATH_PREFIX} )); then + export PATH=$VSCODE_PATH_PREFIX$PATH + fi + builtin unset VSCODE_PATH_PREFIX +fi diff --git a/src/plugins/terminal/shellintegrations/shellintegration-rc.zsh b/src/plugins/terminal/shellintegrations/shellintegration-rc.zsh new file mode 100644 index 00000000000..df4109131a9 --- /dev/null +++ b/src/plugins/terminal/shellintegrations/shellintegration-rc.zsh @@ -0,0 +1,160 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +builtin autoload -Uz add-zsh-hook + +# Prevent the script recursing when setting up +if [ -n "$VSCODE_SHELL_INTEGRATION" ]; then + ZDOTDIR=$USER_ZDOTDIR + builtin return +fi + +# This variable allows the shell to both detect that VS Code's shell integration is enabled as well +# as disable it by unsetting the variable. +VSCODE_SHELL_INTEGRATION=1 + +# By default, zsh will set the $HISTFILE to the $ZDOTDIR location automatically. In the case of the +# shell integration being injected, this means that the terminal will use a different history file +# to other terminals. To fix this issue, set $HISTFILE back to the default location before ~/.zshrc +# is called as that may depend upon the value. +if [[ "$VSCODE_INJECTION" == "1" ]]; then + HISTFILE=$USER_ZDOTDIR/.zsh_history +fi + +# Only fix up ZDOTDIR if shell integration was injected (not manually installed) and has not been called yet +if [[ "$VSCODE_INJECTION" == "1" ]]; then + if [[ $options[norcs] = off && -f $USER_ZDOTDIR/.zshrc ]]; then + VSCODE_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$USER_ZDOTDIR + # A user's custom HISTFILE location might be set when their .zshrc file is sourced below + . $USER_ZDOTDIR/.zshrc + fi +fi + +# Shell integration was disabled by the shell, exit without warning assuming either the shell has +# explicitly disabled shell integration as it's incompatible or it implements the protocol. +if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then + builtin return +fi + +# The property (P) and command (E) codes embed values which require escaping. +# Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex. +__vsc_escape_value() { + builtin emulate -L zsh + + # Process text byte by byte, not by codepoint. + builtin local LC_ALL=C str="$1" i byte token out='' + + for (( i = 0; i < ${#str}; ++i )); do + byte="${str:$i:1}" + + # Escape backslashes and semi-colons + if [ "$byte" = "\\" ]; then + token="\\\\" + elif [ "$byte" = ";" ]; then + token="\\x3b" + else + token="$byte" + fi + + out+="$token" + done + + builtin print -r "$out" +} + +__vsc_in_command_execution="1" +__vsc_current_command="" + +__vsc_prompt_start() { + builtin printf '\e]633;A\a' +} + +__vsc_prompt_end() { + builtin printf '\e]633;B\a' +} + +__vsc_update_cwd() { + builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "${PWD}")" +} + +__vsc_command_output_start() { + builtin printf '\e]633;C\a' + builtin printf '\e]633;E;%s\a' "${__vsc_current_command}" +} + +__vsc_continuation_start() { + builtin printf '\e]633;F\a' +} + +__vsc_continuation_end() { + builtin printf '\e]633;G\a' +} + +__vsc_right_prompt_start() { + builtin printf '\e]633;H\a' +} + +__vsc_right_prompt_end() { + builtin printf '\e]633;I\a' +} + +__vsc_command_complete() { + if [[ "$__vsc_current_command" == "" ]]; then + builtin printf '\e]633;D\a' + else + builtin printf '\e]633;D;%s\a' "$__vsc_status" + fi + __vsc_update_cwd +} + +if [[ -o NOUNSET ]]; then + if [ -z "${RPROMPT-}" ]; then + RPROMPT="" + fi +fi +__vsc_update_prompt() { + __vsc_prior_prompt="$PS1" + __vsc_prior_prompt2="$PS2" + __vsc_in_command_execution="" + PS1="%{$(__vsc_prompt_start)%}$PS1%{$(__vsc_prompt_end)%}" + PS2="%{$(__vsc_continuation_start)%}$PS2%{$(__vsc_continuation_end)%}" + if [ -n "$RPROMPT" ]; then + __vsc_prior_rprompt="$RPROMPT" + RPROMPT="%{$(__vsc_right_prompt_start)%}$RPROMPT%{$(__vsc_right_prompt_end)%}" + fi +} + +__vsc_precmd() { + local __vsc_status="$?" + if [ -z "${__vsc_in_command_execution-}" ]; then + # not in command execution + __vsc_command_output_start + fi + + __vsc_command_complete "$__vsc_status" + __vsc_current_command="" + + # in command execution + if [ -n "$__vsc_in_command_execution" ]; then + # non null + __vsc_update_prompt + fi +} + +__vsc_preexec() { + PS1="$__vsc_prior_prompt" + PS2="$__vsc_prior_prompt2" + if [ -n "$RPROMPT" ]; then + RPROMPT="$__vsc_prior_rprompt" + fi + __vsc_in_command_execution="1" + __vsc_current_command=$2 + __vsc_command_output_start +} +add-zsh-hook precmd __vsc_precmd +add-zsh-hook preexec __vsc_preexec + +if [[ $options[login] = off && $USER_ZDOTDIR != $VSCODE_ZDOTDIR ]]; then + ZDOTDIR=$USER_ZDOTDIR +fi diff --git a/src/plugins/terminal/shellintegrations/shellintegration.fish b/src/plugins/terminal/shellintegrations/shellintegration.fish new file mode 100644 index 00000000000..7495bab3f40 --- /dev/null +++ b/src/plugins/terminal/shellintegrations/shellintegration.fish @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT +# +# Visual Studio Code terminal integration for fish +# +# Manual installation: +# +# (1) Add the following to the end of `$__fish_config_dir/config.fish`: +# +# string match -q "$TERM_PROGRAM" "vscode" +# and . (code --locate-shell-integration-path fish) +# +# (2) Restart fish. + +# Don't run in scripts, other terminals, or more than once per session. +status is-interactive +and string match --quiet "$TERM_PROGRAM" "vscode" +and ! set --query VSCODE_SHELL_INTEGRATION +or exit + +set --global VSCODE_SHELL_INTEGRATION 1 + +# Apply any explicit path prefix (see #99878) +if status --is-login; and set -q VSCODE_PATH_PREFIX + fish_add_path -p $VSCODE_PATH_PREFIX +end +set -e VSCODE_PATH_PREFIX + +# Helper function +function __vsc_esc -d "Emit escape sequences for VS Code shell integration" + builtin printf "\e]633;%s\a" (string join ";" $argv) +end + +# Sent right before executing an interactive command. +# Marks the beginning of command output. +function __vsc_cmd_executed --on-event fish_preexec + __vsc_esc C + __vsc_esc E (__vsc_escape_value "$argv") + + # Creates a marker to indicate a command was run. + set --global _vsc_has_cmd +end + + +# Escape a value for use in the 'P' ("Property") or 'E' ("Command Line") sequences. +# Backslashes are doubled and non-alphanumeric characters are hex encoded. +function __vsc_escape_value + # Escape backslashes and semi-colons + echo $argv \ + | string replace --all '\\' '\\\\' \ + | string replace --all ';' '\\x3b' \ + ; +end + +# Sent right after an interactive command has finished executing. +# Marks the end of command output. +function __vsc_cmd_finished --on-event fish_postexec + __vsc_esc D $status +end + +# Sent when a command line is cleared or reset, but no command was run. +# Marks the cleared line with neither success nor failure. +function __vsc_cmd_clear --on-event fish_cancel + __vsc_esc D +end + +# Sent whenever a new fish prompt is about to be displayed. +# Updates the current working directory. +function __vsc_update_cwd --on-event fish_prompt + __vsc_esc P Cwd=(__vsc_escape_value "$PWD") + + # If a command marker exists, remove it. + # Otherwise, the commandline is empty and no command was run. + if set --query _vsc_has_cmd + set --erase _vsc_has_cmd + else + __vsc_cmd_clear + end +end + +# Sent at the start of the prompt. +# Marks the beginning of the prompt (and, implicitly, a new line). +function __vsc_fish_prompt_start + __vsc_esc A +end + +# Sent at the end of the prompt. +# Marks the beginning of the user's command input. +function __vsc_fish_cmd_start + __vsc_esc B +end + +function __vsc_fish_has_mode_prompt -d "Returns true if fish_mode_prompt is defined and not empty" + functions fish_mode_prompt | string match -rvq '^ *(#|function |end$|$)' +end + +# Preserve the user's existing prompt, to wrap in our escape sequences. +functions --copy fish_prompt __vsc_fish_prompt + +# Preserve and wrap fish_mode_prompt (which appears to the left of the regular +# prompt), but only if it's not defined as an empty function (which is the +# officially documented way to disable that feature). +if __vsc_fish_has_mode_prompt + functions --copy fish_mode_prompt __vsc_fish_mode_prompt + + function fish_mode_prompt + __vsc_fish_prompt_start + __vsc_fish_mode_prompt + end + + function fish_prompt + __vsc_fish_prompt + __vsc_fish_cmd_start + end +else + # No fish_mode_prompt, so put everything in fish_prompt. + function fish_prompt + __vsc_fish_prompt_start + __vsc_fish_prompt + __vsc_fish_cmd_start + end +end diff --git a/src/plugins/terminal/shellintegrations/shellintegration.ps1 b/src/plugins/terminal/shellintegrations/shellintegration.ps1 new file mode 100644 index 00000000000..4fd978a8844 --- /dev/null +++ b/src/plugins/terminal/shellintegrations/shellintegration.ps1 @@ -0,0 +1,158 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +# Prevent installing more than once per session +if (Test-Path variable:global:__VSCodeOriginalPrompt) { + return; +} + +# Disable shell integration when the language mode is restricted +if ($ExecutionContext.SessionState.LanguageMode -ne "FullLanguage") { + return; +} + +$Global:__VSCodeOriginalPrompt = $function:Prompt + +$Global:__LastHistoryId = -1 + +function Global:__VSCode-Escape-Value([string]$value) { + # NOTE: In PowerShell v6.1+, this can be written `$value -replace '…', { … }` instead of `[regex]::Replace`. + # Replace any non-alphanumeric characters. + [regex]::Replace($value, '[\\\n;]', { param($match) + # Encode the (ascii) matches as `\x` + -Join ( + [System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ } + ) + }) +} + +function Global:Prompt() { + $FakeCode = [int]!$global:? + # NOTE: We disable strict mode for the scope of this function because it unhelpfully throws an + # error when $LastHistoryEntry is null, and is not otherwise useful. + Set-StrictMode -Off + $LastHistoryEntry = Get-History -Count 1 + # Skip finishing the command if the first command has not yet started + if ($Global:__LastHistoryId -ne -1) { + if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) { + # Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command) + $Result = "$([char]0x1b)]633;E`a" + $Result += "$([char]0x1b)]633;D`a" + } else { + # Command finished command line + # OSC 633 ; A ; ST + $Result = "$([char]0x1b)]633;E;" + # Sanitize the command line to ensure it can get transferred to the terminal and can be parsed + # correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter + # to only be composed of _printable_ characters as per the spec. + if ($LastHistoryEntry.CommandLine) { + $CommandLine = $LastHistoryEntry.CommandLine + } else { + $CommandLine = "" + } + $Result += $(__VSCode-Escape-Value $CommandLine) + $Result += "`a" + # Command finished exit code + # OSC 633 ; D [; ] ST + $Result += "$([char]0x1b)]633;D;$FakeCode`a" + } + } + # Prompt started + # OSC 633 ; A ST + $Result += "$([char]0x1b)]633;A`a" + # Current working directory + # OSC 633 ; = ST + $Result += if($pwd.Provider.Name -eq 'FileSystem'){"$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a"} + # Before running the original prompt, put $? back to what it was: + if ($FakeCode -ne 0) { + Write-Error "failure" -ea ignore + } + # Run the original prompt + $Result += $Global:__VSCodeOriginalPrompt.Invoke() + # Write command started + $Result += "$([char]0x1b)]633;B`a" + $Global:__LastHistoryId = $LastHistoryEntry.Id + return $Result +} + +# Only send the command executed sequence when PSReadLine is loaded, if not shell integration should +# still work thanks to the command line sequence +if (Get-Module -Name PSReadLine) { + $__VSCodeOriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine + function Global:PSConsoleHostReadLine { + $tmp = $__VSCodeOriginalPSConsoleHostReadLine.Invoke() + # Write command executed sequence directly to Console to avoid the new line from Write-Host + [Console]::Write("$([char]0x1b)]633;C`a") + $tmp + } +} + +# Set IsWindows property +[Console]::Write("$([char]0x1b)]633;P;IsWindows=$($IsWindows)`a") + +# Set always on key handlers which map to default VS Code keybindings +function Set-MappedKeyHandler { + param ([string[]] $Chord, [string[]]$Sequence) + try { + $Handler = Get-PSReadLineKeyHandler -Chord $Chord | Select-Object -First 1 + } catch [System.Management.Automation.ParameterBindingException] { + # PowerShell 5.1 ships with PSReadLine 2.0.0 which does not have -Chord, + # so we check what's bound and filter it. + $Handler = Get-PSReadLineKeyHandler -Bound | Where-Object -FilterScript { $_.Key -eq $Chord } | Select-Object -First 1 + } + if ($Handler) { + Set-PSReadLineKeyHandler -Chord $Sequence -Function $Handler.Function + } +} + +function Set-MappedKeyHandlers { + Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a' + Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b' + Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c' + Set-MappedKeyHandler -Chord Shift+End -Sequence 'F12,d' + + # Conditionally enable suggestions + if ($env:VSCODE_SUGGEST -eq '1') { + Remove-Item Env:VSCODE_SUGGEST + + # VS Code send completions request (may override Ctrl+Spacebar) + Set-PSReadLineKeyHandler -Chord 'F12,e' -ScriptBlock { + Send-Completions + } + + # Suggest trigger characters + Set-PSReadLineKeyHandler -Chord "-" -ScriptBlock { + [Microsoft.PowerShell.PSConsoleReadLine]::Insert("-") + Send-Completions + } + } +} + +function Send-Completions { + $commandLine = "" + $cursorIndex = 0 + # TODO: Since fuzzy matching exists, should completions be provided only for character after the + # last space and then filter on the client side? That would let you trigger ctrl+space + # anywhere on a word and have full completions available + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$commandLine, [ref]$cursorIndex) + $completionPrefix = $commandLine + + # Get completions + $result = "`e]633;Completions" + if ($completionPrefix.Length -gt 0) { + # Get and send completions + $completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex + if ($null -ne $completions.CompletionMatches) { + $result += ";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);" + $result += $completions.CompletionMatches | ConvertTo-Json -Compress + } + } + $result += "`a" + + Write-Host -NoNewLine $result +} + +# Register key handlers if PSReadLine is available +if (Get-Module -Name PSReadLine) { + Set-MappedKeyHandlers +} diff --git a/src/plugins/terminal/terminal.qrc b/src/plugins/terminal/terminal.qrc index a4fece92fce..32d717d0f46 100644 --- a/src/plugins/terminal/terminal.qrc +++ b/src/plugins/terminal/terminal.qrc @@ -1,6 +1,13 @@ - - images/settingscategory_terminal.png - images/settingscategory_terminal@2x.png - + + images/settingscategory_terminal.png + images/settingscategory_terminal@2x.png + shellintegrations/shellintegration-bash.sh + shellintegrations/shellintegration-env.zsh + shellintegrations/shellintegration-login.zsh + shellintegrations/shellintegration-profile.zsh + shellintegrations/shellintegration-rc.zsh + shellintegrations/shellintegration.fish + shellintegrations/shellintegration.ps1 + diff --git a/src/plugins/terminal/terminalpane.cpp b/src/plugins/terminal/terminalpane.cpp index 3ad8fc713a5..215a3883c40 100644 --- a/src/plugins/terminal/terminalpane.cpp +++ b/src/plugins/terminal/terminalpane.cpp @@ -188,13 +188,29 @@ void TerminalPane::setupTerminalWidget(TerminalWidget *terminal) auto setTabText = [this](TerminalWidget * terminal) { auto index = m_tabWidget->indexOf(terminal); - m_tabWidget->setTabText(index, terminal->shellName()); + const FilePath cwd = terminal->cwd(); + + const QString exe = terminal->currentCommand().isEmpty() ? terminal->shellName() + : terminal->currentCommand().executable().fileName(); + + if (cwd.isEmpty()) + m_tabWidget->setTabText(index, exe); + else + m_tabWidget->setTabText(index, exe + " - " + cwd.fileName()); }; connect(terminal, &TerminalWidget::started, [setTabText, terminal](qint64 /*pid*/) { setTabText(terminal); }); + connect(terminal, &TerminalWidget::cwdChanged, [setTabText, terminal]() { + setTabText(terminal); + }); + + connect(terminal, &TerminalWidget::commandChanged, [setTabText, terminal]() { + setTabText(terminal); + }); + if (!terminal->shellName().isEmpty()) setTabText(terminal); diff --git a/src/plugins/terminal/terminalsurface.cpp b/src/plugins/terminal/terminalsurface.cpp index ad47fd8b15b..128c05213d6 100644 --- a/src/plugins/terminal/terminalsurface.cpp +++ b/src/plugins/terminal/terminalsurface.cpp @@ -23,10 +23,13 @@ QColor toQColor(const VTermColor &c) struct TerminalSurfacePrivate { - TerminalSurfacePrivate(TerminalSurface *surface, const QSize &initialGridSize) + TerminalSurfacePrivate(TerminalSurface *surface, + const QSize &initialGridSize, + ShellIntegration *shellIntegration) : m_vterm(vterm_new(initialGridSize.height(), initialGridSize.width()), vterm_free) , m_vtermScreen(vterm_obtain_screen(m_vterm.get())) , m_scrollback(std::make_unique(5000)) + , m_shellIntegration(shellIntegration) , q(surface) {} @@ -75,7 +78,15 @@ struct TerminalSurfacePrivate vterm_screen_set_damage_merge(m_vtermScreen, VTERM_DAMAGE_SCROLL); vterm_screen_enable_altscreen(m_vtermScreen, true); + memset(&m_vtermStateFallbacks, 0, sizeof(m_vtermStateFallbacks)); + + m_vtermStateFallbacks.osc = [](int cmd, VTermStringFragment fragment, void *user) { + auto p = static_cast(user); + return p->osc(cmd, fragment); + }; + VTermState *vts = vterm_obtain_state(m_vterm.get()); + vterm_state_set_unrecognised_fallbacks(vts, &m_vtermStateFallbacks, this); vterm_state_set_bold_highbright(vts, true); vterm_screen_reset(m_vtermScreen, 1); @@ -196,6 +207,14 @@ struct TerminalSurfacePrivate return 1; } + int osc(int cmd, const VTermStringFragment &fragment) + { + if (m_shellIntegration) + m_shellIntegration->onOsc(cmd, fragment); + + return 1; + } + int setTerminalProperties(VTermProp prop, VTermValue *val) { switch (prop) { @@ -274,19 +293,23 @@ struct TerminalSurfacePrivate std::unique_ptr m_vterm; VTermScreen *m_vtermScreen; VTermScreenCallbacks m_vtermScreenCallbacks; + VTermStateFallbacks m_vtermStateFallbacks; QColor m_defaultBgColor; Cursor m_cursor; + QString m_currentCommand; bool m_altscreen{false}; std::unique_ptr m_scrollback; + ShellIntegration *m_shellIntegration{nullptr}; + TerminalSurface *q; }; -TerminalSurface::TerminalSurface(QSize initialGridSize) - : d(std::make_unique(this, initialGridSize)) +TerminalSurface::TerminalSurface(QSize initialGridSize, ShellIntegration *shellIntegration) + : d(std::make_unique(this, initialGridSize, shellIntegration)) { d->init(); } @@ -478,6 +501,11 @@ QColor TerminalSurface::defaultBgColor() const return toQColor(d->defaultBgColor()); } +ShellIntegration *TerminalSurface::shellIntegration() const +{ + return d->m_shellIntegration; +} + CellIterator TerminalSurface::begin() const { auto res = CellIterator(this, {0, 0}); diff --git a/src/plugins/terminal/terminalsurface.h b/src/plugins/terminal/terminalsurface.h index 354a3a028d4..04958582c27 100644 --- a/src/plugins/terminal/terminalsurface.h +++ b/src/plugins/terminal/terminalsurface.h @@ -4,6 +4,7 @@ #pragma once #include "celliterator.h" +#include "shellintegration.h" #include #include @@ -47,7 +48,7 @@ class TerminalSurface : public QObject Q_OBJECT; public: - TerminalSurface(QSize initialGridSize); + TerminalSurface(QSize initialGridSize, ShellIntegration *shellIntegration); ~TerminalSurface(); public: @@ -95,6 +96,8 @@ public: QColor defaultBgColor() const; + ShellIntegration *shellIntegration() const; + signals: void writeToPty(const QByteArray &data); void invalidated(QRect grid); diff --git a/src/plugins/terminal/terminalwidget.cpp b/src/plugins/terminal/terminalwidget.cpp index 23da5088ce9..b168320dce5 100644 --- a/src/plugins/terminal/terminalwidget.cpp +++ b/src/plugins/terminal/terminalwidget.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -132,6 +133,10 @@ void TerminalWidget::setupPty() m_process->setWorkingDirectory(*m_openParameters.workingDirectory); m_process->setEnvironment(env); + if (m_surface->shellIntegration()) { + m_surface->shellIntegration()->prepareProcess(*m_process.get()); + } + connect(m_process.get(), &QtcProcess::readyReadStandardOutput, this, [this]() { onReadyRead(false); }); @@ -242,7 +247,8 @@ void TerminalWidget::setupActions() connect(&m_zoomInAction, &QAction::triggered, this, &TerminalWidget::zoomIn); connect(&m_zoomOutAction, &QAction::triggered, this, &TerminalWidget::zoomOut); - addActions({&m_copyAction, &m_pasteAction, &m_clearSelectionAction, &m_zoomInAction, &m_zoomOutAction}); + addActions( + {&m_copyAction, &m_pasteAction, &m_clearSelectionAction, &m_zoomInAction, &m_zoomOutAction}); } void TerminalWidget::writeToPty(const QByteArray &data) @@ -253,7 +259,8 @@ void TerminalWidget::writeToPty(const QByteArray &data) void TerminalWidget::setupSurface() { - m_surface = std::make_unique(QSize{80, 60}); + m_shellIntegration.reset(new ShellIntegration()); + m_surface = std::make_unique(QSize{80, 60}, m_shellIntegration.get()); connect(m_surface.get(), &Internal::TerminalSurface::writeToPty, @@ -299,6 +306,22 @@ void TerminalWidget::setupSurface() connect(m_surface.get(), &Internal::TerminalSurface::unscroll, this, [this] { verticalScrollBar()->setValue(verticalScrollBar()->maximum()); }); + if (m_shellIntegration) { + connect(m_shellIntegration.get(), + &ShellIntegration::commandChanged, + this, + [this](const CommandLine &command) { + m_currentCommand = command; + emit commandChanged(m_currentCommand); + }); + connect(m_shellIntegration.get(), + &ShellIntegration::currentDirChanged, + this, + [this](const QString ¤tDir) { + m_cwd = FilePath::fromUserInput(currentDir); + emit cwdChanged(m_cwd); + }); + } } void TerminalWidget::configBlinkTimer() @@ -470,6 +493,16 @@ QString TerminalWidget::shellName() const return m_shellName; } +FilePath TerminalWidget::cwd() const +{ + return m_cwd; +} + +CommandLine TerminalWidget::currentCommand() const +{ + return m_currentCommand; +} + QPoint TerminalWidget::viewportToGlobal(QPoint p) const { int y = p.y() - topMargin(); diff --git a/src/plugins/terminal/terminalwidget.h b/src/plugins/terminal/terminalwidget.h index 53d82b3899d..60cff4183fd 100644 --- a/src/plugins/terminal/terminalwidget.h +++ b/src/plugins/terminal/terminalwidget.h @@ -72,8 +72,13 @@ public: QString shellName() const; + Utils::FilePath cwd() const; + Utils::CommandLine currentCommand() const; + signals: void started(qint64 pid); + void cwdChanged(const Utils::FilePath &cwd); + void commandChanged(const Utils::CommandLine &cmd); protected: void paintEvent(QPaintEvent *event) override; @@ -158,6 +163,7 @@ protected: private: std::unique_ptr m_process; std::unique_ptr m_surface; + std::unique_ptr m_shellIntegration; QString m_shellName; @@ -201,6 +207,9 @@ private: Internal::Cursor m_cursor; QTimer m_cursorBlinkTimer; bool m_cursorBlinkState{true}; + + Utils::FilePath m_cwd; + Utils::CommandLine m_currentCommand; }; } // namespace Terminal diff --git a/src/plugins/terminal/tests/integration b/src/plugins/terminal/tests/integration new file mode 100755 index 00000000000..ac17432cb66 --- /dev/null +++ b/src/plugins/terminal/tests/integration @@ -0,0 +1,70 @@ +#!/bin/bash + +echo "Testing integration response, best start this from a terminal that has no builtin integration" +echo "e.g. 'sh'" +echo + +echo -e "\033[1m ⎆ Current dir should have changed to '/Some/Dir/Here'\033[0m" +printf "\033]7;file:///Some/Dir/Here\033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo + +echo -e "\033[1m ⎆ Current dir should have changed to '/Some/Other/Dir/Here'\033[0m" +printf "\033]1337;CurrentDir=/Some/Other/Dir/Here\033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo + + +echo -e "\033[1m ⎆ Current dir should have changed to '/VSCode/dir/with space'\033[0m" +printf "\033]633P;Cwd=/VSCode/dir/with space\033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo + +echo -e "\033[1m ⎆ The current process should have changed to 'test'\033[0m" +printf "\033]633E;test with arguments\033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo + +echo -e "\033[1m ⎆ The current process should have changed to 'test with space'\033[0m" +printf "\033]633E;'test with space'\033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo + +echo -e "\033[1m ⎆ The current process should have changed to 'test with space v2'\033[0m" +printf "\033]633E;\"test with space v2\"\033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo + +echo -e "\033[1m ⎆ The current process should have changed to 'test with space v3'\033[0m" +printf "\033]633E;\"./test/test with space v3\" -argument\033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo + +echo -e "\033[1m ⎆ The current process should have changed to 'cat'\033[0m" +printf "\033]633E;cat /dev/random | base64 -argument\033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo + +echo -e "\033[1m ⎆ The current process should have changed to 'cat me'\033[0m" +printf "\033]633E;cat\\ me args \033\\" + +read -p " ⎆ Press enter to continue " -n1 -s +echo +echo +