@@ -315,16 +315,28 @@ public async Task<bool> TryStartAsync(HostStartOptions startOptions, Cancellatio
315
315
_logger . LogDebug ( "Profiles loaded!" ) ;
316
316
}
317
317
318
- if ( startOptions . ShellIntegrationEnabled )
318
+ if ( ! string . IsNullOrEmpty ( startOptions . ShellIntegrationScript ) )
319
319
{
320
- _logger . LogDebug ( "Enabling shell integration ..." ) ;
320
+ _logger . LogDebug ( "Enabling Terminal Shell Integration ..." ) ;
321
321
_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 ) ;
323
335
_logger . LogDebug ( "Shell integration enabled!" ) ;
324
336
}
325
337
else
326
338
{
327
- _logger . LogDebug ( "Shell integration not enabled!" ) ;
339
+ _logger . LogDebug ( "Terminal Shell Integration not enabled!" ) ;
328
340
}
329
341
330
342
await _started . Task . ConfigureAwait ( false ) ;
@@ -495,6 +507,7 @@ public Task ExecuteDelegateAsync(
495
507
new SynchronousDelegateTask ( _logger , representation , executionOptions , action , cancellationToken ) ) ;
496
508
}
497
509
510
+ // TODO: One day fix these so the cancellation token is last.
498
511
public Task < IReadOnlyList < TResult > > ExecutePSCommandAsync < TResult > (
499
512
PSCommand psCommand ,
500
513
CancellationToken cancellationToken ,
@@ -581,209 +594,12 @@ internal Task LoadHostProfilesAsync(CancellationToken cancellationToken)
581
594
cancellationToken ) ;
582
595
}
583
596
584
- private Task EnableShellIntegrationAsync ( CancellationToken cancellationToken )
597
+ private Task EnableShellIntegrationAsync ( string shellIntegrationScript , CancellationToken cancellationToken )
585
598
{
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 } ) ;
787
603
}
788
604
789
605
public Task SetInitialWorkingDirectoryAsync ( string path , CancellationToken cancellationToken )
@@ -1262,16 +1078,34 @@ private void InvokeInput(string input, CancellationToken cancellationToken)
1262
1078
1263
1079
try
1264
1080
{
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 .
1268
1084
if ( _shellIntegrationEnabled )
1269
1085
{
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 = \" { 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 ) ;
1271
1105
}
1272
1106
1273
1107
InvokePSCommand (
1274
- new PSCommand ( ) . AddScript ( input , useLocalScope : false ) ,
1108
+ new PSCommand ( ) . AddScript ( input ) ,
1275
1109
new PowerShellExecutionOptions
1276
1110
{
1277
1111
AddToHistory = true ,
0 commit comments