@@ -77,6 +77,8 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns
77
77
78
78
private string _localComputerName ;
79
79
80
+ private bool _shellIntegrationEnabled ;
81
+
80
82
private ConsoleKeyInfo ? _lastKey ;
81
83
82
84
private bool _skipNextPrompt ;
@@ -254,6 +256,18 @@ public async Task<bool> TryStartAsync(HostStartOptions startOptions, Cancellatio
254
256
_logger . LogDebug ( "Profiles loaded!" ) ;
255
257
}
256
258
259
+ if ( startOptions . ShellIntegrationEnabled )
260
+ {
261
+ _logger . LogDebug ( "Enabling shell integration..." ) ;
262
+ _shellIntegrationEnabled = true ;
263
+ await EnableShellIntegrationAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
264
+ _logger . LogDebug ( "Shell integration enabled!" ) ;
265
+ }
266
+ else
267
+ {
268
+ _logger . LogDebug ( "Shell integration not enabled!" ) ;
269
+ }
270
+
257
271
if ( startOptions . InitialWorkingDirectory is not null )
258
272
{
259
273
_logger . LogDebug ( $ "Setting InitialWorkingDirectory to { startOptions . InitialWorkingDirectory } ...") ;
@@ -487,6 +501,96 @@ internal Task LoadHostProfilesAsync(CancellationToken cancellationToken)
487
501
cancellationToken ) ;
488
502
}
489
503
504
+ private Task EnableShellIntegrationAsync ( CancellationToken cancellationToken )
505
+ {
506
+ // Imported on 11/17/22 from
507
+ // https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1
508
+ // with quotes escaped, `__VSCodeOriginalPSConsoleHostReadLine` removed (as it's done
509
+ // in our own ReadLine function), and `[Console]::Write` replaced with `Write-Host`.
510
+ // TODO: We can probably clean some of this up.
511
+ const string shellIntegrationScript = @"
512
+ # Prevent installing more than once per session
513
+ if (Test-Path variable:global:__VSCodeOriginalPrompt) {
514
+ return;
515
+ }
516
+
517
+ # Disable shell integration when the language mode is restricted
518
+ if ($ExecutionContext.SessionState.LanguageMode -ne ""FullLanguage"") {
519
+ return;
520
+ }
521
+
522
+ $Global:__VSCodeOriginalPrompt = $function:Prompt
523
+
524
+ $Global:__LastHistoryId = -1
525
+
526
+
527
+ function Global:Prompt() {
528
+ $FakeCode = [int]!$global:?
529
+ $LastHistoryEntry = Get-History -Count 1
530
+ # Skip finishing the command if the first command has not yet started
531
+ if ($Global:__LastHistoryId -ne -1) {
532
+ if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) {
533
+ # Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command)
534
+ $Result = ""`e]633;E`a""
535
+ $Result += ""`e]633;D`a""
536
+ } else {
537
+ # Command finished command line
538
+ # OSC 633 ; A ; <CommandLine?> ST
539
+ $Result = ""`e]633;E;""
540
+ # Sanitize the command line to ensure it can get transferred to the terminal and can be parsed
541
+ # correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter
542
+ # to only be composed of _printable_ characters as per the spec.
543
+ if ($LastHistoryEntry.CommandLine) {
544
+ $CommandLine = $LastHistoryEntry.CommandLine
545
+ } else {
546
+ $CommandLine = """"
547
+ }
548
+ $Result += $CommandLine.Replace(""\"", ""\\"").Replace(""`n"", ""\x0a"").Replace("";"", ""\x3b"")
549
+ $Result += ""`a""
550
+ # Command finished exit code
551
+ # OSC 633 ; D [; <ExitCode>] ST
552
+ $Result += ""`e]633;D;$FakeCode`a""
553
+ }
554
+ }
555
+ # Prompt started
556
+ # OSC 633 ; A ST
557
+ $Result += ""`e]633;A`a""
558
+ # Current working directory
559
+ # OSC 633 ; <Property>=<Value> ST
560
+ $Result += if($pwd.Provider.Name -eq 'FileSystem'){""`e]633;P;Cwd=$($pwd.ProviderPath)`a""}
561
+ # Before running the original prompt, put $? back to what it was:
562
+ if ($FakeCode -ne 0) { Write-Error ""failure"" -ea ignore }
563
+ # Run the original prompt
564
+ $Result += $Global:__VSCodeOriginalPrompt.Invoke()
565
+ # Write command started
566
+ $Result += ""`e]633;B`a""
567
+ $Global:__LastHistoryId = $LastHistoryEntry.Id
568
+ return $Result
569
+ }
570
+
571
+ # Set IsWindows property
572
+ Write-Host -NoNewLine ""`e]633;P;IsWindows=$($IsWindows)`a""
573
+
574
+ # Set always on key handlers which map to default VS Code keybindings
575
+ function Set-MappedKeyHandler {
576
+ param ([string[]] $Chord, [string[]]$Sequence)
577
+ $Handler = $(Get-PSReadLineKeyHandler -Chord $Chord | Select-Object -First 1)
578
+ if ($Handler) {
579
+ Set-PSReadLineKeyHandler -Chord $Sequence -Function $Handler.Function
580
+ }
581
+ }
582
+ function Set-MappedKeyHandlers {
583
+ Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a'
584
+ Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b'
585
+ Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c'
586
+ Set-MappedKeyHandler -Chord Shift+End -Sequence 'F12,d'
587
+ }
588
+ Set-MappedKeyHandlers
589
+ " ;
590
+
591
+ return ExecutePSCommandAsync ( new PSCommand ( ) . AddScript ( shellIntegrationScript ) , cancellationToken ) ;
592
+ }
593
+
490
594
public Task SetInitialWorkingDirectoryAsync ( string path , CancellationToken cancellationToken )
491
595
{
492
596
return Directory . Exists ( path )
@@ -962,8 +1066,17 @@ private string InvokeReadLine(CancellationToken cancellationToken)
962
1066
private void InvokeInput ( string input , CancellationToken cancellationToken )
963
1067
{
964
1068
SetBusy ( true ) ;
1069
+
965
1070
try
966
1071
{
1072
+ // For VS Code's shell integration feature, this replaces their
1073
+ // PSConsoleHostReadLine function wrapper, as that global function is not available
1074
+ // to users of PSES, since we already wrap ReadLine ourselves.
1075
+ if ( _shellIntegrationEnabled )
1076
+ {
1077
+ System . Console . Write ( "\x1b ]633;C\a " ) ;
1078
+ }
1079
+
967
1080
InvokePSCommand (
968
1081
new PSCommand ( ) . AddScript ( input , useLocalScope : false ) ,
969
1082
new PowerShellExecutionOptions
0 commit comments