Skip to content

Commit 27b279e

Browse files
committed
Update shell integration script to fix command decorations
Happy new year! Updated one year ago today, it needed it again. This has actually been broken for a while unfortunately because the last time we manually patched it to disable strict mode, we did so before capturing the exit code (and thus it was lost). Fixed now at least.
1 parent 1905151 commit 27b279e

File tree

1 file changed

+113
-18
lines changed

1 file changed

+113
-18
lines changed

src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs

+113-18
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ internal Task LoadHostProfilesAsync(CancellationToken cancellationToken)
583583

584584
private Task EnableShellIntegrationAsync(CancellationToken cancellationToken)
585585
{
586-
// Imported on 01/03/23 from
586+
// Imported on 01/03/24 from
587587
// https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1
588588
// with quotes escaped, `__VSCodeOriginalPSConsoleHostReadLine` removed (as it's done
589589
// in our own ReadLine function), and `[Console]::Write` replaced with `Write-Host`.
@@ -602,42 +602,74 @@ private Task EnableShellIntegrationAsync(CancellationToken cancellationToken)
602602
603603
$Global:__LastHistoryId = -1
604604
605+
# Store the nonce in script scope and unset the global
606+
$Nonce = $env:VSCODE_NONCE
607+
$env:VSCODE_NONCE = $null
608+
609+
if ($env:VSCODE_ENV_REPLACE) {
610+
$Split = $env:VSCODE_ENV_REPLACE.Split("":"")
611+
foreach ($Item in $Split) {
612+
$Inner = $Item.Split('=')
613+
[Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':'))
614+
}
615+
$env:VSCODE_ENV_REPLACE = $null
616+
}
617+
if ($env:VSCODE_ENV_PREPEND) {
618+
$Split = $env:VSCODE_ENV_PREPEND.Split("":"")
619+
foreach ($Item in $Split) {
620+
$Inner = $Item.Split('=')
621+
[Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':') + [Environment]::GetEnvironmentVariable($Inner[0]))
622+
}
623+
$env:VSCODE_ENV_PREPEND = $null
624+
}
625+
if ($env:VSCODE_ENV_APPEND) {
626+
$Split = $env:VSCODE_ENV_APPEND.Split("":"")
627+
foreach ($Item in $Split) {
628+
$Inner = $Item.Split('=')
629+
[Environment]::SetEnvironmentVariable($Inner[0], [Environment]::GetEnvironmentVariable($Inner[0]) + $Inner[1].Replace('\x3a', ':'))
630+
}
631+
$env:VSCODE_ENV_APPEND = $null
632+
}
633+
605634
function Global:__VSCode-Escape-Value([string]$value) {
606635
# NOTE: In PowerShell v6.1+, this can be written `$value -replace '…', { … }` instead of `[regex]::Replace`.
607636
# Replace any non-alphanumeric characters.
608637
[regex]::Replace($value, '[\\\n;]', { param($match)
609-
# Encode the (ascii) matches as `\x<hex>`
610-
-Join (
611-
[System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ }
612-
)
613-
})
638+
# Encode the (ascii) matches as `\x<hex>`
639+
-Join (
640+
[System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ }
641+
)
642+
})
614643
}
615644
616645
function Global:Prompt() {
646+
$FakeCode = [int]!$global:?
617647
# NOTE: We disable strict mode for the scope of this function because it unhelpfully throws an
618648
# error when $LastHistoryEntry is null, and is not otherwise useful.
619649
Set-StrictMode -Off
620-
$FakeCode = [int]!$global:?
621650
$LastHistoryEntry = Get-History -Count 1
622651
# Skip finishing the command if the first command has not yet started
623652
if ($Global:__LastHistoryId -ne -1) {
624653
if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) {
625654
# Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command)
626-
$Result = ""$([char]0x1b)]633;E`a""
655+
$Result = ""$([char]0x1b)]633;E`a""
627656
$Result += ""$([char]0x1b)]633;D`a""
628-
} else {
657+
}
658+
else {
629659
# Command finished command line
630-
# OSC 633 ; A ; <CommandLine?> ST
631-
$Result = ""$([char]0x1b)]633;E;""
660+
# OSC 633 ; E ; <CommandLine?> ; <Nonce?> ST
661+
$Result = ""$([char]0x1b)]633;E;""
632662
# Sanitize the command line to ensure it can get transferred to the terminal and can be parsed
633663
# correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter
634664
# to only be composed of _printable_ characters as per the spec.
635665
if ($LastHistoryEntry.CommandLine) {
636666
$CommandLine = $LastHistoryEntry.CommandLine
637-
} else {
667+
}
668+
else {
638669
$CommandLine = """"
639670
}
640671
$Result += $(__VSCode-Escape-Value $CommandLine)
672+
$Result += "";$Nonce""
641673
$Result += ""`a""
642674
# Command finished exit code
643675
# OSC 633 ; D [; <ExitCode>] ST
@@ -649,7 +681,7 @@ private Task EnableShellIntegrationAsync(CancellationToken cancellationToken)
649681
$Result += ""$([char]0x1b)]633;A`a""
650682
# Current working directory
651683
# OSC 633 ; <Property>=<Value> ST
652-
$Result += if($pwd.Provider.Name -eq 'FileSystem'){""$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a""}
684+
$Result += if ($pwd.Provider.Name -eq 'FileSystem') { ""$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a"" }
653685
# Before running the original prompt, put $? back to what it was:
654686
if ($FakeCode -ne 0) {
655687
Write-Error ""failure"" -ea ignore
@@ -664,28 +696,91 @@ private Task EnableShellIntegrationAsync(CancellationToken cancellationToken)
664696
665697
# Set IsWindows property
666698
if ($PSVersionTable.PSVersion -lt ""6.0"") {
667-
[Console]::Write(""$([char]0x1b)]633;P;IsWindows=$true`a"")
668-
} else {
669-
[Console]::Write(""$([char]0x1b)]633;P;IsWindows=$IsWindows`a"")
699+
# Windows PowerShell is only available on Windows
700+
Write-Host -NoNewLine ""$([char]0x1b)]633;P;IsWindows=$true`a""
701+
}
702+
else {
703+
Write-Host -NoNewLine ""$([char]0x1b)]633;P;IsWindows=$IsWindows`a""
670704
}
671705
672706
# Set always on key handlers which map to default VS Code keybindings
673707
function Set-MappedKeyHandler {
674708
param ([string[]] $Chord, [string[]]$Sequence)
675-
$Handler = $(Get-PSReadLineKeyHandler -Chord $Chord | Select-Object -First 1)
709+
try {
710+
$Handler = Get-PSReadLineKeyHandler -Chord $Chord | Select-Object -First 1
711+
}
712+
catch [System.Management.Automation.ParameterBindingException] {
713+
# PowerShell 5.1 ships with PSReadLine 2.0.0 which does not have -Chord,
714+
# so we check what's bound and filter it.
715+
$Handler = Get-PSReadLineKeyHandler -Bound | Where-Object -FilterScript { $_.Key -eq $Chord } | Select-Object -First 1
716+
}
676717
if ($Handler) {
677718
Set-PSReadLineKeyHandler -Chord $Sequence -Function $Handler.Function
678719
}
679720
}
680721
722+
$Global:__VSCodeHaltCompletions = $false
681723
function Set-MappedKeyHandlers {
682724
Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a'
683725
Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b'
684726
Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c'
685727
Set-MappedKeyHandler -Chord Shift+End -Sequence 'F12,d'
728+
729+
# Conditionally enable suggestions
730+
if ($env:VSCODE_SUGGEST -eq '1') {
731+
Remove-Item Env:VSCODE_SUGGEST
732+
733+
# VS Code send completions request (may override Ctrl+Spacebar)
734+
Set-PSReadLineKeyHandler -Chord 'F12,e' -ScriptBlock {
735+
Send-Completions
736+
}
737+
738+
# Suggest trigger characters
739+
Set-PSReadLineKeyHandler -Chord ""-"" -ScriptBlock {
740+
[Microsoft.PowerShell.PSConsoleReadLine]::Insert(""-"")
741+
if (!$Global:__VSCodeHaltCompletions) {
742+
Send-Completions
743+
}
744+
}
745+
746+
Set-PSReadLineKeyHandler -Chord 'F12,y' -ScriptBlock {
747+
$Global:__VSCodeHaltCompletions = $true
748+
}
749+
750+
Set-PSReadLineKeyHandler -Chord 'F12,z' -ScriptBlock {
751+
$Global:__VSCodeHaltCompletions = $false
752+
}
753+
}
754+
}
755+
756+
function Send-Completions {
757+
$commandLine = """"
758+
$cursorIndex = 0
759+
# TODO: Since fuzzy matching exists, should completions be provided only for character after the
760+
# last space and then filter on the client side? That would let you trigger ctrl+space
761+
# anywhere on a word and have full completions available
762+
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$commandLine, [ref]$cursorIndex)
763+
$completionPrefix = $commandLine
764+
765+
# Get completions
766+
$result = ""`e]633;Completions""
767+
if ($completionPrefix.Length -gt 0) {
768+
# Get and send completions
769+
$completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex
770+
if ($null -ne $completions.CompletionMatches) {
771+
$result += "";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);""
772+
$result += $completions.CompletionMatches | ConvertTo-Json -Compress
773+
}
774+
}
775+
$result += ""`a""
776+
777+
Write-Host -NoNewLine $result
686778
}
687779
688-
Set-MappedKeyHandlers
780+
# Register key handlers if PSReadLine is available
781+
if (Get-Module -Name PSReadLine) {
782+
Set-MappedKeyHandlers
783+
}
689784
";
690785

691786
return ExecutePSCommandAsync(new PSCommand().AddScript(shellIntegrationScript), cancellationToken);

0 commit comments

Comments
 (0)