Skip to content

Commit 07ffe63

Browse files
committed
WIP: Enable VS Code's shell integration
It seems to work, but needs more testing.
1 parent 015fb46 commit 07ffe63

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-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

+118
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,95 @@ 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+
const string shellIntegrationScript = @"
511+
# Prevent installing more than once per session
512+
if (Test-Path variable:global:__VSCodeOriginalPrompt) {
513+
return;
514+
}
515+
516+
# Disable shell integration when the language mode is restricted
517+
if ($ExecutionContext.SessionState.LanguageMode -ne ""FullLanguage"") {
518+
return;
519+
}
520+
521+
$Global:__VSCodeOriginalPrompt = $function:Prompt
522+
523+
$Global:__LastHistoryId = -1
524+
525+
526+
function Global:Prompt() {
527+
$FakeCode = [int]!$global:?
528+
$LastHistoryEntry = Get-History -Count 1
529+
# Skip finishing the command if the first command has not yet started
530+
if ($Global:__LastHistoryId -ne -1) {
531+
if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) {
532+
# Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command)
533+
$Result = ""`e]633;E`a""
534+
$Result += ""`e]633;D`a""
535+
} else {
536+
# Command finished command line
537+
# OSC 633 ; A ; <CommandLine?> ST
538+
$Result = ""`e]633;E;""
539+
# Sanitize the command line to ensure it can get transferred to the terminal and can be parsed
540+
# correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter
541+
# to only be composed of _printable_ characters as per the spec.
542+
if ($LastHistoryEntry.CommandLine) {
543+
$CommandLine = $LastHistoryEntry.CommandLine
544+
} else {
545+
$CommandLine = """"
546+
}
547+
$Result += $CommandLine.Replace(""\"", ""\\"").Replace(""`n"", ""\x0a"").Replace("";"", ""\x3b"")
548+
$Result += ""`a""
549+
# Command finished exit code
550+
# OSC 633 ; D [; <ExitCode>] ST
551+
$Result += ""`e]633;D;$FakeCode`a""
552+
}
553+
}
554+
# Prompt started
555+
# OSC 633 ; A ST
556+
$Result += ""`e]633;A`a""
557+
# Current working directory
558+
# OSC 633 ; <Property>=<Value> ST
559+
$Result += if($pwd.Provider.Name -eq 'FileSystem'){""`e]633;P;Cwd=$($pwd.ProviderPath)`a""}
560+
# Before running the original prompt, put $? back to what it was:
561+
if ($FakeCode -ne 0) { Write-Error ""failure"" -ea ignore }
562+
# Run the original prompt
563+
$Result += $Global:__VSCodeOriginalPrompt.Invoke()
564+
# Write command started
565+
$Result += ""`e]633;B`a""
566+
$Global:__LastHistoryId = $LastHistoryEntry.Id
567+
return $Result
568+
}
569+
570+
# Set IsWindows property
571+
Write-Host -NoNewLine ""`e]633;P;IsWindows=$($IsWindows)`a""
572+
573+
# Set always on key handlers which map to default VS Code keybindings
574+
function Set-MappedKeyHandler {
575+
param ([string[]] $Chord, [string[]]$Sequence)
576+
$Handler = $(Get-PSReadLineKeyHandler -Chord $Chord | Select-Object -First 1)
577+
if ($Handler) {
578+
Set-PSReadLineKeyHandler -Chord $Sequence -Function $Handler.Function
579+
}
580+
}
581+
function Set-MappedKeyHandlers {
582+
Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a'
583+
Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b'
584+
Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c'
585+
Set-MappedKeyHandler -Chord Shift+End -Sequence 'F12,d'
586+
}
587+
Set-MappedKeyHandlers
588+
";
589+
590+
return ExecutePSCommandAsync(new PSCommand().AddScript(shellIntegrationScript), cancellationToken);
591+
}
592+
490593
public Task SetInitialWorkingDirectoryAsync(string path, CancellationToken cancellationToken)
491594
{
492595
return Directory.Exists(path)
@@ -962,8 +1065,23 @@ private string InvokeReadLine(CancellationToken cancellationToken)
9621065
private void InvokeInput(string input, CancellationToken cancellationToken)
9631066
{
9641067
SetBusy(true);
1068+
9651069
try
9661070
{
1071+
if (_shellIntegrationEnabled)
1072+
{
1073+
InvokePSCommand(
1074+
new PSCommand().AddScript("Write-Host -NoNewLine \"`e]633;C`a\""),
1075+
new PowerShellExecutionOptions
1076+
{
1077+
AddToHistory = false,
1078+
ThrowOnError = false,
1079+
WriteOutputToHost = true,
1080+
FromRepl = true,
1081+
},
1082+
cancellationToken);
1083+
}
1084+
9671085
InvokePSCommand(
9681086
new PSCommand().AddScript(input, useLocalScope: false),
9691087
new PowerShellExecutionOptions

0 commit comments

Comments
 (0)