@@ -317,14 +317,28 @@ public async Task<bool> TryStartAsync(HostStartOptions startOptions, Cancellatio
317317
318318 if ( startOptions . ShellIntegrationEnabled )
319319 {
320- _logger . LogDebug ( "Enabling shell integration..." ) ;
320+ _logger . LogDebug ( "Enabling VS Code's shell integration..." ) ;
321321 _shellIntegrationEnabled = true ;
322- await EnableShellIntegrationAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
322+ // Hopefully we were passed `process.argv0` from the VS Code client correctly,
323+ // otherwise try assuming it's just in the PATH.
324+ string code = startOptions . ClientExePath ?? "code" ;
325+ // TODO: Make the __psEditorServices prefix shared (it's used elsewhere too).
326+ string shellIntegrationScript = $$ """
327+ # Setup VS Code's Terminal Shell Integration.
328+
329+ # Define fake PSConsoleHostReadLine so VS Code's script can execute it to get the user's input.
330+ $global:__psEditorServices_userInput = "";
331+ function global:PSConsoleHostReadLine { $global:__psEditorServices_userInput }
332+
333+ # Find and execute VS Code's built-in shell integration script.
334+ try { . "$(& "{{ code }} " --locate-shell-integration-path pwsh)" } catch {}
335+ """ ;
336+ await EnableShellIntegrationAsync ( shellIntegrationScript , cancellationToken ) . ConfigureAwait ( false ) ;
323337 _logger . LogDebug ( "Shell integration enabled!" ) ;
324338 }
325339 else
326340 {
327- _logger . LogDebug ( "Shell integration not enabled!" ) ;
341+ _logger . LogDebug ( "VS Code's shell integration not enabled!" ) ;
328342 }
329343
330344 await _started . Task . ConfigureAwait ( false ) ;
@@ -495,6 +509,7 @@ public Task ExecuteDelegateAsync(
495509 new SynchronousDelegateTask ( _logger , representation , executionOptions , action , cancellationToken ) ) ;
496510 }
497511
512+ // TODO: One day fix these so the cancellation token is last.
498513 public Task < IReadOnlyList < TResult > > ExecutePSCommandAsync < TResult > (
499514 PSCommand psCommand ,
500515 CancellationToken cancellationToken ,
@@ -581,210 +596,14 @@ internal Task LoadHostProfilesAsync(CancellationToken cancellationToken)
581596 cancellationToken ) ;
582597 }
583598
584- private Task EnableShellIntegrationAsync ( CancellationToken cancellationToken )
585- {
586- // Imported on 01/03/24 from
587- // https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1
588- // with quotes escaped, `__VSCodeOriginalPSConsoleHostReadLine` removed (as it's done
589- // in our own ReadLine function), and `[Console]::Write` replaced with `Write-Host`.
590- const string shellIntegrationScript = @"
591- # Prevent installing more than once per session
592- if (Test-Path variable:global:__VSCodeOriginalPrompt) {
593- return;
594- }
595-
596- # Disable shell integration when the language mode is restricted
597- if ($ExecutionContext.SessionState.LanguageMode -ne ""FullLanguage"") {
598- return;
599- }
600-
601- $Global:__VSCodeOriginalPrompt = $function:Prompt
602-
603- $Global:__LastHistoryId = -1
604-
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-
634- function Global:__VSCode-Escape-Value([string]$value) {
635- # NOTE: In PowerShell v6.1+, this can be written `$value -replace '…', { … }` instead of `[regex]::Replace`.
636- # Replace any non-alphanumeric characters.
637- [regex]::Replace($value, '[\\\n;]', { param($match)
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- })
643- }
644-
645- function Global:Prompt() {
646- $FakeCode = [int]!$global:?
647- # NOTE: We disable strict mode for the scope of this function because it unhelpfully throws an
648- # error when $LastHistoryEntry is null, and is not otherwise useful.
649- Set-StrictMode -Off
650- $LastHistoryEntry = Get-History -Count 1
651- # Skip finishing the command if the first command has not yet started
652- if ($Global:__LastHistoryId -ne -1) {
653- if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) {
654- # Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command)
655- $Result = ""$([char]0x1b)]633;E`a""
656- $Result += ""$([char]0x1b)]633;D`a""
657- }
658- else {
659- # Command finished command line
660- # OSC 633 ; E ; <CommandLine?> ; <Nonce?> ST
661- $Result = ""$([char]0x1b)]633;E;""
662- # Sanitize the command line to ensure it can get transferred to the terminal and can be parsed
663- # correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter
664- # to only be composed of _printable_ characters as per the spec.
665- if ($LastHistoryEntry.CommandLine) {
666- $CommandLine = $LastHistoryEntry.CommandLine
667- }
668- else {
669- $CommandLine = """"
670- }
671- $Result += $(__VSCode-Escape-Value $CommandLine)
672- $Result += "";$Nonce""
673- $Result += ""`a""
674- # Command finished exit code
675- # OSC 633 ; D [; <ExitCode>] ST
676- $Result += ""$([char]0x1b)]633;D;$FakeCode`a""
677- }
678- }
679- # Prompt started
680- # OSC 633 ; A ST
681- $Result += ""$([char]0x1b)]633;A`a""
682- # Current working directory
683- # OSC 633 ; <Property>=<Value> ST
684- $Result += if ($pwd.Provider.Name -eq 'FileSystem') { ""$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a"" }
685- # Before running the original prompt, put $? back to what it was:
686- if ($FakeCode -ne 0) {
687- Write-Error ""failure"" -ea ignore
688- }
689- # Run the original prompt
690- $Result += $Global:__VSCodeOriginalPrompt.Invoke()
691- # Write command started
692- $Result += ""$([char]0x1b)]633;B`a""
693- $Global:__LastHistoryId = $LastHistoryEntry.Id
694- return $Result
695- }
696-
697- # Set IsWindows property
698- if ($PSVersionTable.PSVersion -lt ""6.0"") {
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""
704- }
705-
706- # Set always on key handlers which map to default VS Code keybindings
707- function Set-MappedKeyHandler {
708- param ([string[]] $Chord, [string[]]$Sequence)
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- }
717- if ($Handler) {
718- Set-PSReadLineKeyHandler -Chord $Sequence -Function $Handler.Function
719- }
720- }
721-
722- $Global:__VSCodeHaltCompletions = $false
723- function Set-MappedKeyHandlers {
724- Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a'
725- Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b'
726- Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c'
727- 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
778- }
779-
780- # Register key handlers if PSReadLine is available
781- if (Get-Module -Name PSReadLine) {
782- Set-MappedKeyHandlers
783- }
784- " ;
785-
786- return ExecutePSCommandAsync ( new PSCommand ( ) . AddScript ( shellIntegrationScript ) , cancellationToken ) ;
787- }
599+ private Task EnableShellIntegrationAsync ( string shellIntegrationScript , CancellationToken cancellationToken ) => ExecutePSCommandAsync (
600+ new PSCommand ( ) . AddScript ( shellIntegrationScript ) ,
601+ cancellationToken ,
602+ new PowerShellExecutionOptions
603+ {
604+ AddToHistory = true , // VS Code adds theirs to the history, so we do too.
605+ ThrowOnError = false
606+ } ) ;
788607
789608 public Task SetInitialWorkingDirectoryAsync ( string path , CancellationToken cancellationToken )
790609 {
@@ -1262,16 +1081,36 @@ private void InvokeInput(string input, CancellationToken cancellationToken)
12621081
12631082 try
12641083 {
1265- // For VS Code's shell integration feature, this replaces their
1266- // PSConsoleHostReadLine function wrapper, as that global function is not available
1267- // to users of PSES, since we already wrap ReadLine ourselves .
1084+ // For VS Code's shell integration feature, we call PSConsoleHostReadLine specially as they've wrapped it.
1085+ // Normally it would not be available (since we wrap ReadLine ourselves),
1086+ // but in this case we've made the original just emit the user's input so that their wrapper works as intended .
12681087 if ( _shellIntegrationEnabled )
12691088 {
1270- System . Console . Write ( "\x1b ]633;C\a " ) ;
1089+ // Save the user's input to our special global variable so PSConsoleHostReadLine can read it.
1090+ InvokePSCommand (
1091+ new PSCommand ( ) . AddScript ( $ "$global:__psEditorServices_userInput = \" ${ input } \" ") ,
1092+ new PowerShellExecutionOptions { ThrowOnError = false } ,
1093+ cancellationToken ) ;
1094+
1095+ // Invoke VS Code's PSConsoleHostReadLine wrapper.
1096+ InvokePSCommand (
1097+ new PSCommand ( ) . AddScript ( "PSConsoleHostReadLine" ) ,
1098+ new PowerShellExecutionOptions
1099+ {
1100+ ThrowOnError = false ,
1101+ WriteOutputToHost = true ,
1102+ } ,
1103+ cancellationToken ) ;
1104+
1105+ // Reset our global variable.
1106+ InvokePSCommand (
1107+ new PSCommand ( ) . AddScript ( "$global:__psEditorServices_userInput = \" \" " ) ,
1108+ new PowerShellExecutionOptions { ThrowOnError = false } ,
1109+ cancellationToken ) ;
12711110 }
12721111
12731112 InvokePSCommand (
1274- new PSCommand ( ) . AddScript ( input , useLocalScope : false ) ,
1113+ new PSCommand ( ) . AddScript ( input ) ,
12751114 new PowerShellExecutionOptions
12761115 {
12771116 AddToHistory = true ,
0 commit comments