diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs index 7daa19628..83828aa31 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs @@ -9,5 +9,7 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console internal interface IReadLine { string ReadLine(CancellationToken cancellationToken); + + void AddToHistory(string historyEntry); } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs index cda3af925..c9d643402 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs @@ -38,6 +38,8 @@ public override string ReadLine(CancellationToken cancellationToken) => _psesHos InvokePSReadLine, cancellationToken); + public override void AddToHistory(string historyEntry) => _psrlProxy.AddToHistory(historyEntry); + protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) => _psesHost.ReadKey(intercept: true, cancellationToken); private string InvokePSReadLine(CancellationToken cancellationToken) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs index 8d05edc18..7d5f8231b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs @@ -9,6 +9,12 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console internal abstract class TerminalReadLine : IReadLine { + public virtual void AddToHistory(string historyEntry) + { + // No-op by default. If the ReadLine provider is not PSRL then history is automatically + // added as part of the invocation process. + } + public abstract string ReadLine(CancellationToken cancellationToken); protected abstract ConsoleKeyInfo ReadKey(CancellationToken cancellationToken); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs index dbcab3a9c..10bef621b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs @@ -34,5 +34,6 @@ public record PowerShellExecutionOptions : ExecutionOptions public bool WriteInputToHost { get; init; } public bool ThrowOnError { get; init; } = true; public bool AddToHistory { get; init; } + internal bool FromRepl { get; init; } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs index 354827d94..6c3f3092c 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -95,6 +95,7 @@ private static bool IsPromptCommand(PSCommand command) private IReadOnlyList ExecuteNormally(CancellationToken cancellationToken) { _frame = _psesHost.CurrentFrame; + MaybeAddToHistory(_psCommand); if (PowerShellExecutionOptions.WriteOutputToHost) { _psCommand.AddOutputCommand(); @@ -187,6 +188,7 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT cancellationToken.Register(CancelDebugExecution); PSDataCollection outputCollection = new(); + MaybeAddToHistory(_psCommand); // Out-Default doesn't work as needed in the debugger // Instead we add Out-String to the command and collect results in a PSDataCollection @@ -353,6 +355,33 @@ private void CancelNormalExecution() } } + private void MaybeAddToHistory(PSCommand command) + { + // Do not add PSES internal commands to history. Also exclude input that came from the + // REPL (e.g. PSReadLine) as it handles history itself in that scenario. + if (PowerShellExecutionOptions is { AddToHistory: false } or { FromRepl: true }) + { + return; + } + + // Only add pure script commands with no arguments to interactive history. + if (command.Commands is { Count: not 1 } + || command.Commands[0] is { Parameters.Count: not 0 } or { IsScript: false }) + { + return; + } + + try + { + _psesHost.AddToHistory(command.Commands[0].CommandText); + } + catch + { + // Ignore exceptions as the user can register a scriptblock predicate that + // determines if the command should be added to history. + } + } + private void CancelDebugExecution() { if (_pwsh.Runspace.RunspaceStateInfo.IsUsable()) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index d86b56254..8dcce509b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -446,6 +446,8 @@ public void InvokePSDelegate(string representation, ExecutionOptions executionOp task.ExecuteAndGetResult(cancellationToken); } + internal void AddToHistory(string historyEntry) => _readLineProvider.ReadLine.AddToHistory(historyEntry); + internal Task LoadHostProfilesAsync(CancellationToken cancellationToken) { // NOTE: This is a special task run on startup! @@ -918,7 +920,8 @@ private void InvokeInput(string input, CancellationToken cancellationToken) { AddToHistory = true, ThrowOnError = false, - WriteOutputToHost = true + WriteOutputToHost = true, + FromRepl = true, }, cancellationToken); }