@@ -315,16 +315,28 @@ public async Task<bool> TryStartAsync(HostStartOptions startOptions, Cancellatio
315315 _logger . LogDebug ( "Profiles loaded!" ) ;
316316 }
317317
318- if ( startOptions . ShellIntegrationEnabled )
318+ if ( ! string . IsNullOrEmpty ( startOptions . ShellIntegrationScript ) )
319319 {
320- _logger . LogDebug ( "Enabling shell integration ..." ) ;
320+ _logger . LogDebug ( "Enabling Terminal Shell Integration ..." ) ;
321321 _shellIntegrationEnabled = true ;
322- await EnableShellIntegrationAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
322+ // TODO: Make the __psEditorServices prefix shared (it's used elsewhere too).
323+ string setupShellIntegration = $$ """
324+ # Setup Terminal Shell Integration.
325+
326+ # Define a fake PSConsoleHostReadLine so the integration script's wrapper
327+ # can execute it to get the user's input.
328+ $global:__psEditorServices_userInput = "";
329+ function global:PSConsoleHostReadLine { $global:__psEditorServices_userInput }
330+
331+ # Execute the provided shell integration script.
332+ try { . '{{ startOptions . ShellIntegrationScript }} ' } catch {}
333+ """ ;
334+ await EnableShellIntegrationAsync ( setupShellIntegration , cancellationToken ) . ConfigureAwait ( false ) ;
323335 _logger . LogDebug ( "Shell integration enabled!" ) ;
324336 }
325337 else
326338 {
327- _logger . LogDebug ( "Shell integration not enabled!" ) ;
339+ _logger . LogDebug ( "Terminal Shell Integration not enabled!" ) ;
328340 }
329341
330342 await _started . Task . ConfigureAwait ( false ) ;
@@ -495,6 +507,7 @@ public Task ExecuteDelegateAsync(
495507 new SynchronousDelegateTask ( _logger , representation , executionOptions , action , cancellationToken ) ) ;
496508 }
497509
510+ // TODO: One day fix these so the cancellation token is last.
498511 public Task < IReadOnlyList < TResult > > ExecutePSCommandAsync < TResult > (
499512 PSCommand psCommand ,
500513 CancellationToken cancellationToken ,
@@ -581,209 +594,12 @@ internal Task LoadHostProfilesAsync(CancellationToken cancellationToken)
581594 cancellationToken ) ;
582595 }
583596
584- private Task EnableShellIntegrationAsync ( CancellationToken cancellationToken )
597+ private Task EnableShellIntegrationAsync ( string shellIntegrationScript , CancellationToken cancellationToken )
585598 {
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 ) ;
599+ return ExecutePSCommandAsync (
600+ new PSCommand ( ) . AddScript ( shellIntegrationScript ) ,
601+ cancellationToken ,
602+ new PowerShellExecutionOptions { AddToHistory = false , ThrowOnError = false } ) ;
787603 }
788604
789605 public Task SetInitialWorkingDirectoryAsync ( string path , CancellationToken cancellationToken )
@@ -1262,16 +1078,34 @@ private void InvokeInput(string input, CancellationToken cancellationToken)
12621078
12631079 try
12641080 {
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 .
1081+ // For the terminal shell integration feature, we call PSConsoleHostReadLine specially as it's been wrapped.
1082+ // Normally it would not be available (since we wrap ReadLine ourselves),
1083+ // but in this case we've made the original just emit the user's input so that the wrapper works as intended .
12681084 if ( _shellIntegrationEnabled )
12691085 {
1270- System . Console . Write ( "\x1b ]633;C\a " ) ;
1086+ // Save the user's input to our special global variable so PSConsoleHostReadLine can read it.
1087+ InvokePSCommand (
1088+ new PSCommand ( ) . AddScript ( "$global:__psEditorServices_userInput = $args[0]" ) . AddArgument ( input ) ,
1089+ new PowerShellExecutionOptions { ThrowOnError = false , WriteOutputToHost = false } ,
1090+ cancellationToken ) ;
1091+
1092+ // Invoke the PSConsoleHostReadLine wrapper. We don't write the output because it
1093+ // returns the command line (user input) which would then be duplicate noise. Fortunately
1094+ // it writes the shell integration sequences directly using [Console]::Write.
1095+ InvokePSCommand (
1096+ new PSCommand ( ) . AddScript ( "PSConsoleHostReadLine" ) ,
1097+ new PowerShellExecutionOptions { ThrowOnError = false , WriteOutputToHost = false } ,
1098+ cancellationToken ) ;
1099+
1100+ // Reset our global variable.
1101+ InvokePSCommand (
1102+ new PSCommand ( ) . AddScript ( "$global:__psEditorServices_userInput = \" \" " ) ,
1103+ new PowerShellExecutionOptions { ThrowOnError = false , WriteOutputToHost = false } ,
1104+ cancellationToken ) ;
12711105 }
12721106
12731107 InvokePSCommand (
1274- new PSCommand ( ) . AddScript ( input , useLocalScope : false ) ,
1108+ new PSCommand ( ) . AddScript ( input ) ,
12751109 new PowerShellExecutionOptions
12761110 {
12771111 AddToHistory = true ,
0 commit comments