Skip to content

Commit fb41949

Browse files
committed
Enable VS Code's shell integration
It seems to work, but needs more testing.
1 parent d2e6e71 commit fb41949

File tree

3 files changed

+118
-2
lines changed

3 files changed

+118
-2
lines changed

src/PowerShellEditorServices/Server/PsesLanguageServer.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ public async Task StartAsync()
152152
LoadProfiles = initializationOptions?.GetValue("enableProfileLoading")?.Value<bool>() ?? true,
153153
// TODO: Consider deprecating the setting which sets this and
154154
// instead use WorkspacePath exclusively.
155-
InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value<string>() ?? workspaceService.WorkspacePath
155+
InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value<string>() ?? workspaceService.WorkspacePath,
156+
ShellIntegrationEnabled = initializationOptions?.GetValue("shellIntegrationEnabled")?.Value<bool>() ?? false
156157
};
157158

158159
_psesHost = languageServer.Services.GetService<PsesInternalHost>();

src/PowerShellEditorServices/Services/PowerShell/Host/HostStartOptions.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ internal struct HostStartOptions
88
public bool LoadProfiles { get; set; }
99

1010
public string InitialWorkingDirectory { get; set; }
11-
}
11+
12+
public bool ShellIntegrationEnabled { get; set; }
13+
}
1214
}

src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs

+113
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)