@@ -77,6 +77,8 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns
7777
7878 private string _localComputerName ;
7979
80+ private bool _shellIntegrationEnabled ;
81+
8082 private ConsoleKeyInfo ? _lastKey ;
8183
8284 private bool _skipNextPrompt ;
@@ -254,6 +256,18 @@ public async Task<bool> TryStartAsync(HostStartOptions startOptions, Cancellatio
254256 _logger . LogDebug ( "Profiles loaded!" ) ;
255257 }
256258
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+
257271 if ( startOptions . InitialWorkingDirectory is not null )
258272 {
259273 _logger . LogDebug ( $ "Setting InitialWorkingDirectory to { startOptions . InitialWorkingDirectory } ...") ;
@@ -487,6 +501,96 @@ internal Task LoadHostProfilesAsync(CancellationToken cancellationToken)
487501 cancellationToken ) ;
488502 }
489503
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+
490594 public Task SetInitialWorkingDirectoryAsync ( string path , CancellationToken cancellationToken )
491595 {
492596 return Directory . Exists ( path )
@@ -962,8 +1066,17 @@ private string InvokeReadLine(CancellationToken cancellationToken)
9621066 private void InvokeInput ( string input , CancellationToken cancellationToken )
9631067 {
9641068 SetBusy ( true ) ;
1069+
9651070 try
9661071 {
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+
9671080 InvokePSCommand (
9681081 new PSCommand ( ) . AddScript ( input , useLocalScope : false ) ,
9691082 new PowerShellExecutionOptions
0 commit comments