# --------------------------------------------------------------------------------------------- # 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.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 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 $__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 } 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` -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.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 [; ] 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" } # Send current environment variables as JSON # OSC 633 ; EnvJson ; ; 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 ; = 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") } # 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) { [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 [; [; ]] 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 } } 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' # Enable suggestions if the environment variable is set and Windows PowerShell is not being used # as APIs are not available to support this feature if ($env:VSCODE_SUGGEST -eq '1' -and $PSVersionTable.PSVersion -ge "7.0") { Remove-Item Env:VSCODE_SUGGEST # VS Code send completions request (may override Ctrl+Spacebar) Set-PSReadLineKeyHandler -Chord 'F12,e' -ScriptBlock { Send-Completions } } } function Send-Completions { $commandLine = "" $cursorIndex = 0 $prefixCursorDelta = 0 [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$commandLine, [ref]$cursorIndex) $completionPrefix = $commandLine # Start completions sequence $result = "$([char]0x1b)]633;Completions" # Only provide completions for arguments and defer to TabExpansion2. # `[` is included here as namespace commands are not included in CompleteCommand(''), # additionally for some reason CompleteVariable('[') causes the prompt to clear and reprint # multiple times if ($completionPrefix.Contains(' ')) { # Adjust the completion prefix and cursor index such that tab expansion will be requested # immediately after the last whitespace. This allows the client to perform fuzzy filtering # such that requesting completions in the middle of a word should show the same completions # as at the start. This only happens when the last word does not include special characters: # - `-`: Completion change when flags are used. # - `/` and `\`: Completions change when navigating directories. # - `$`: Completions change when variables. $lastWhitespaceIndex = $completionPrefix.LastIndexOf(' ') $lastWord = $completionPrefix.Substring($lastWhitespaceIndex + 1) if ($lastWord -match '^-') { $newCursorIndex = $lastWhitespaceIndex + 2 $completionPrefix = $completionPrefix.Substring(0, $newCursorIndex) $prefixCursorDelta = $cursorIndex - $newCursorIndex $cursorIndex = $newCursorIndex } elseif ($lastWord -notmatch '[/\\$]') { if ($lastWhitespaceIndex -ne -1 -and $lastWhitespaceIndex -lt $cursorIndex) { $newCursorIndex = $lastWhitespaceIndex + 1 $completionPrefix = $completionPrefix.Substring(0, $newCursorIndex) $prefixCursorDelta = $cursorIndex - $newCursorIndex $cursorIndex = $newCursorIndex } } # If it contains `/` or `\`, get completions from the nearest `/` or `\` such that file # completions are consistent regardless of where it was requested elseif ($lastWord -match '[/\\]') { $lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\')) if ($lastSlashIndex -ne -1 -and $lastSlashIndex -lt $cursorIndex) { $newCursorIndex = $lastSlashIndex + 1 $completionPrefix = $completionPrefix.Substring(0, $newCursorIndex) $prefixCursorDelta = $cursorIndex - $newCursorIndex $cursorIndex = $newCursorIndex } } # Get completions using TabExpansion2 $completions = $null $completionMatches = $null try { $completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex $completionMatches = $completions.CompletionMatches | Where-Object { $_.ResultType -ne [System.Management.Automation.CompletionResultType]::ProviderContainer -and $_.ResultType -ne [System.Management.Automation.CompletionResultType]::ProviderItem } } catch { # TabExpansion2 may throw when there are no completions, in this case return an empty # list to prevent falling back to file path completions } if ($null -eq $completions -or $null -eq $completionMatches) { $result += ";0;$($completionPrefix.Length);$($completionPrefix.Length);[]" } else { $result += ";$($completions.ReplacementIndex);$($completions.ReplacementLength + $prefixCursorDelta);$($cursorIndex - $prefixCursorDelta);" $json = [System.Collections.ArrayList]@($completionMatches) $mappedCommands = Compress-Completions($json) $result += $mappedCommands | ConvertTo-Json -Compress } } # End completions sequence $result += "`a" Write-Host -NoNewLine $result } function Compress-Completions($completions) { $completions | ForEach-Object { if ($_.CustomIcon) { ,@($_.CompletionText, $_.ResultType, $_.ToolTip, $_.CustomIcon) } elseif ($_.CompletionText -eq $_.ToolTip) { ,@($_.CompletionText, $_.ResultType) } else { ,@($_.CompletionText, $_.ResultType, $_.ToolTip) } } } # Register key handlers if PSReadLine is available if (Get-Module -Name PSReadLine) { Set-MappedKeyHandlers }