Path: blob/main/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1
4760 views
# ---------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# ---------------------------------------------------------------------------------------------
# Prevent installing more than once per session
if ((Test-Path variable:global:__VSCodeState) -and $null -ne $Global:__VSCodeState.OriginalPrompt) {
return;
}
# Disable shell integration when the language mode is restricted
if ($ExecutionContext.SessionState.LanguageMode -ne "FullLanguage") {
return;
}
$Global:__VSCodeState = @{
OriginalPrompt = $function:Prompt
LastHistoryId = -1
IsInExecution = $false
EnvVarsToReport = @()
Nonce = $null
IsStable = $null
IsA11yMode = $null
IsWindows10 = $false
}
# Store the nonce in a regular variable and unset the environment variable. It's by design that
# anything that can execute PowerShell code can read the nonce, as it's basically impossible to hide
# in PowerShell. The most important thing is getting it out of the environment.
$Global:__VSCodeState.Nonce = $env:VSCODE_NONCE
$env:VSCODE_NONCE = $null
$Global:__VSCodeState.IsStable = $env:VSCODE_STABLE
$env:VSCODE_STABLE = $null
$Global:__VSCodeState.IsA11yMode = $env:VSCODE_A11Y_MODE
$env:VSCODE_A11Y_MODE = $null
$__vscode_shell_env_reporting = $env:VSCODE_SHELL_ENV_REPORTING
$env:VSCODE_SHELL_ENV_REPORTING = $null
if ($__vscode_shell_env_reporting) {
$Global:__VSCodeState.EnvVarsToReport = $__vscode_shell_env_reporting.Split(',')
}
Remove-Variable -Name __vscode_shell_env_reporting -ErrorAction SilentlyContinue
$osVersion = [System.Environment]::OSVersion.Version
$Global:__VSCodeState.IsWindows10 = $IsWindows -and $osVersion.Major -eq 10 -and $osVersion.Minor -eq 0 -and $osVersion.Build -lt 22000
Remove-Variable -Name osVersion -ErrorAction SilentlyContinue
if ($env:VSCODE_ENV_REPLACE) {
$Split = $env:VSCODE_ENV_REPLACE.Split(":")
foreach ($Item in $Split) {
$Inner = $Item.Split('=', 2)
[Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':'))
}
$env:VSCODE_ENV_REPLACE = $null
}
if ($env:VSCODE_ENV_PREPEND) {
$Split = $env:VSCODE_ENV_PREPEND.Split(":")
foreach ($Item in $Split) {
$Inner = $Item.Split('=', 2)
[Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':') + [Environment]::GetEnvironmentVariable($Inner[0]))
}
$env:VSCODE_ENV_PREPEND = $null
}
if ($env:VSCODE_ENV_APPEND) {
$Split = $env:VSCODE_ENV_APPEND.Split(":")
foreach ($Item in $Split) {
$Inner = $Item.Split('=', 2)
[Environment]::SetEnvironmentVariable($Inner[0], [Environment]::GetEnvironmentVariable($Inner[0]) + $Inner[1].Replace('\x3a', ':'))
}
$env:VSCODE_ENV_APPEND = $null
}
# Register Python shell activate hooks
# Prevent multiple activation with guard
if (-not $env:VSCODE_PYTHON_AUTOACTIVATE_GUARD) {
$env:VSCODE_PYTHON_AUTOACTIVATE_GUARD = '1'
if ($env:VSCODE_PYTHON_PWSH_ACTIVATE -and $env:TERM_PROGRAM -eq 'vscode') {
$activateScript = $env:VSCODE_PYTHON_PWSH_ACTIVATE
Remove-Item Env:VSCODE_PYTHON_PWSH_ACTIVATE
try {
Invoke-Expression $activateScript
$Global:__VSCodeState.OriginalPrompt = $function:Prompt
}
catch {
$activationError = $_
Write-Host "`e[0m`e[7m * `e[0;103m VS Code Python powershell activation failed with exit code $($activationError.Exception.Message) `e[0m"
}
}
}
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, "[$([char]0x00)-$([char]0x1f)\\\n;]", { param($match)
# Encode the (ascii) matches as `\x<hex>`
-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
$Result = ""
# Skip finishing the command if the first command has not yet started or an execution has not
# yet begun
if ($Global:__VSCodeState.LastHistoryId -ne -1 -and ($Global:__VSCodeState.HasPSReadLine -eq $false -or $Global:__VSCodeState.IsInExecution -eq $true)) {
$Global:__VSCodeState.IsInExecution = $false
if ($LastHistoryEntry.Id -eq $Global:__VSCodeState.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;D`a"
}
else {
# Command finished exit code
# OSC 633 ; D [; <ExitCode>] 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 ; <Property>=<Value> ST
$Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a" }
# Send current environment variables as JSON
# OSC 633 ; EnvJson ; <Environment> ; <Nonce>
if ($Global:__VSCodeState.EnvVarsToReport.Count -gt 0) {
$envMap = @{}
foreach ($varName in $Global:__VSCodeState.EnvVarsToReport) {
if (Test-Path "env:$varName") {
$envMap[$varName] = (Get-Item "env:$varName").Value
}
}
$envJson = $envMap | ConvertTo-Json -Compress
$Result += "$([char]0x1b)]633;EnvJson;$(__VSCode-Escape-Value $envJson);$($Global:__VSCodeState.Nonce)`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
$OriginalPrompt += $Global:__VSCodeState.OriginalPrompt.Invoke()
$Result += $OriginalPrompt
# Prompt
# OSC 633 ; <Property>=<Value> ST
if ($Global:__VSCodeState.IsStable -eq "0") {
$Result += "$([char]0x1b)]633;P;Prompt=$(__VSCode-Escape-Value $OriginalPrompt)`a"
}
# Write command started
$Result += "$([char]0x1b)]633;B`a"
$Global:__VSCodeState.LastHistoryId = $LastHistoryEntry.Id
return $Result
}
# Report prompt type
if ($env:STARSHIP_SESSION_KEY) {
[Console]::Write("$([char]0x1b)]633;P;PromptType=starship`a")
}
elseif ($env:POSH_SESSION_ID) {
[Console]::Write("$([char]0x1b)]633;P;PromptType=oh-my-posh`a")
}
elseif ((Test-Path variable:global:GitPromptSettings) -and $Global:GitPromptSettings) {
[Console]::Write("$([char]0x1b)]633;P;PromptType=posh-git`a")
}
if ($Global:__VSCodeState.IsA11yMode -eq "1") {
if (-not (Get-Module -Name PSReadLine)) {
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$specialPsrlPath = Join-Path $scriptRoot 'psreadline'
Import-Module $specialPsrlPath
if (Get-Module -Name PSReadLine) {
Set-PSReadLineOption -EnableScreenReaderMode
}
}
}
# Only send the command executed sequence when PSReadLine is loaded, if not shell integration should
# still work thanks to the command line sequence
$Global:__VSCodeState.HasPSReadLine = $false
if (Get-Module -Name PSReadLine) {
$Global:__VSCodeState.HasPSReadLine = $true
[Console]::Write("$([char]0x1b)]633;P;HasRichCommandDetection=True`a")
$Global:__VSCodeState.OriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine
function Global:PSConsoleHostReadLine {
$CommandLine = $Global:__VSCodeState.OriginalPSConsoleHostReadLine.Invoke()
$Global:__VSCodeState.IsInExecution = $true
# Command line
# OSC 633 ; E [; <CommandLine> [; <Nonce>]] ST
$Result = "$([char]0x1b)]633;E;"
$Result += $(__VSCode-Escape-Value $CommandLine)
# Only send the nonce if the OS is not Windows 10 as it seems to echo to the terminal
# sometimes
if ($Global:__VSCodeState.IsWindows10 -eq $false) {
$Result += ";$($Global:__VSCodeState.Nonce)"
}
$Result += "`a"
# Command executed
# OSC 633 ; C ST
$Result += "$([char]0x1b)]633;C`a"
# Write command executed sequence directly to Console to avoid the new line from Write-Host
[Console]::Write($Result)
$CommandLine
}
# Set ContinuationPrompt property
$Global:__VSCodeState.ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt
if ($Global:__VSCodeState.ContinuationPrompt) {
[Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $Global:__VSCodeState.ContinuationPrompt)`a")
}
}
# Set IsWindows property
if ($PSVersionTable.PSVersion -lt "6.0") {
# Windows PowerShell is only available on Windows
[Console]::Write("$([char]0x1b)]633;P;IsWindows=$true`a")
}
else {
[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
}
}
if ($Global:__VSCodeState.HasPSReadLine) {
# Prevent AI-executed commands from polluting shell history
if ($env:VSCODE_PREVENT_SHELL_HISTORY -eq "1") {
Set-PSReadLineOption -AddToHistoryHandler {
param([string]$line)
return $false
}
$env:VSCODE_PREVENT_SHELL_HISTORY = $null
}
}