diff --git a/src/PrettyPrompt/Panes/CompletionPane.cs b/src/PrettyPrompt/Panes/CompletionPane.cs index d30eeed..fcbb24d 100644 --- a/src/PrettyPrompt/Panes/CompletionPane.cs +++ b/src/PrettyPrompt/Panes/CompletionPane.cs @@ -69,16 +69,26 @@ public CompletionPane( FilteredView.SelectedItemChanged += SelectedItemChanged; } - private void Open() + private async Task Open(CancellationToken cancellationToken) { + bool wasOpen = IsOpen; this.IsOpen = true; this.allCompletions = Array.Empty(); + if (!wasOpen) + { + await promptCallbacks.CompletionWindowOpenedAsync(codePane.Document.GetText(), codePane.Document.Caret, cancellationToken).ConfigureAwait(false); + } } private async Task Close(CancellationToken cancellationToken) { + bool wasOpen = IsOpen; IsOpen = false; await FilteredView.Clear(cancellationToken).ConfigureAwait(false); + if (wasOpen) + { + await promptCallbacks.CompletionWindowClosedAsync(codePane.Document.GetText(), codePane.Document.Caret, cancellationToken).ConfigureAwait(false); + } } async Task IKeyPressHandler.OnKeyDown(KeyPress key, CancellationToken cancellationToken) @@ -135,7 +145,7 @@ End or (_, End) or { if (codePane.Selection is null) { - Open(); + await Open(cancellationToken).ConfigureAwait(false); } key.Handled = true; } @@ -148,7 +158,7 @@ End or (_, End) or if (completionListTriggered) { await Close(cancellationToken).ConfigureAwait(false); - Open(); + await Open(cancellationToken).ConfigureAwait(false); key.Handled = true; } return; @@ -197,7 +207,7 @@ async Task IKeyPressHandler.OnKeyUp(KeyPress key, CancellationToken cancellation !completionListTriggeredOnKeyDown && await promptCallbacks.ShouldOpenCompletionWindowAsync(codePane.Document.GetText(), codePane.Document.Caret, key, cancellationToken).ConfigureAwait(false)) { - Open(); + await Open(cancellationToken).ConfigureAwait(false); } } diff --git a/src/PrettyPrompt/PromptCallbacks.cs b/src/PrettyPrompt/PromptCallbacks.cs index 7ddad55..b91aed3 100644 --- a/src/PrettyPrompt/PromptCallbacks.cs +++ b/src/PrettyPrompt/PromptCallbacks.cs @@ -91,6 +91,22 @@ public interface IPromptCallbacks /// A value indicating whether the completion window should automatically open. Task ShouldOpenCompletionWindowAsync(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken); + /// + /// Invoked after the completion window has been opened. + /// + /// The user's input text + /// The index of the text caret in the input text + /// Cancellation token + Task CompletionWindowOpenedAsync(string text, int caret, CancellationToken cancellationToken); + + /// + /// Invoked after the completion window has been closed. + /// + /// The user's input text + /// The index of the text caret in the input text + /// Cancellation token + Task CompletionWindowClosedAsync(string text, int caret, CancellationToken cancellationToken); + /// /// Optionaly transforms key presses to another ones. /// @@ -184,6 +200,20 @@ Task IPromptCallbacks.ShouldOpenCompletionWindowAsync(string text, int car return ShouldOpenCompletionWindowAsync(text, caret, keyPress, cancellationToken); } + Task IPromptCallbacks.CompletionWindowOpenedAsync(string text, int caret, CancellationToken cancellationToken) + { + Debug.Assert(caret >= 0 && caret <= text.Length); + + return CompletionWindowOpenedAsync(text, caret, cancellationToken); + } + + Task IPromptCallbacks.CompletionWindowClosedAsync(string text, int caret, CancellationToken cancellationToken) + { + Debug.Assert(caret >= 0 && caret <= text.Length); + + return CompletionWindowClosedAsync(text, caret, cancellationToken); + } + Task IPromptCallbacks.TransformKeyPressAsync(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken) { Debug.Assert(caret >= 0 && caret <= text.Length); @@ -284,6 +314,14 @@ protected virtual Task ShouldOpenCompletionWindowAsync(string text, int ca return Task.FromResult(caret - 2 >= 0 && char.IsWhiteSpace(text[caret - 2]) && char.IsLetter(text[caret - 1])); } + /// + protected virtual Task CompletionWindowOpenedAsync(string text, int caret, CancellationToken cancellationToken) + => Task.CompletedTask; + + /// + protected virtual Task CompletionWindowClosedAsync(string text, int caret, CancellationToken cancellationToken) + => Task.CompletedTask; + /// protected virtual Task TransformKeyPressAsync(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken) => Task.FromResult(keyPress); diff --git a/tests/PrettyPrompt.Tests/CompletionTests.cs b/tests/PrettyPrompt.Tests/CompletionTests.cs index 7f181fa..afcd578 100644 --- a/tests/PrettyPrompt.Tests/CompletionTests.cs +++ b/tests/PrettyPrompt.Tests/CompletionTests.cs @@ -112,6 +112,30 @@ public async Task ReadLine_CompletionMenu_Closes() Assert.Equal($"A", result2.Text); } + [Fact] + public async Task ReadLine_CompletionMenu_InvokesOpenedAndClosedCallbacks() + { + var console = ConsoleStub.NewConsole(); + int openedCount = 0; + int closedCount = 0; + var callbacks = new TestPromptCallbacks + { + CompletionCallback = new CompletionTestData().CompletionHandlerAsync, + CompletionWindowOpenedCallback = (_, _) => { openedCount++; return Task.CompletedTask; }, + CompletionWindowClosedCallback = (_, _) => { closedCount++; return Task.CompletedTask; }, + }; + var prompt = new Prompt(callbacks: callbacks, console: console); + + // typing 'A' auto-opens the completion menu, Escape closes it. + console.StubInput($"A{Escape}{Enter}"); + var result = await prompt.ReadLineAsync(); + + Assert.True(result.IsSuccess); + Assert.Equal("A", result.Text); + Assert.Equal(1, openedCount); + Assert.Equal(1, closedCount); + } + [Fact] public async Task ReadLine_CompletionMenu_Scrolls() { diff --git a/tests/PrettyPrompt.Tests/TestPromptCallbacks.cs b/tests/PrettyPrompt.Tests/TestPromptCallbacks.cs index 29d7bcb..837fb94 100644 --- a/tests/PrettyPrompt.Tests/TestPromptCallbacks.cs +++ b/tests/PrettyPrompt.Tests/TestPromptCallbacks.cs @@ -12,6 +12,7 @@ namespace PrettyPrompt.Tests; internal delegate Task SpanToReplaceByCompletionCallbackAsync(string text, int caret); internal delegate Task> CompletionCallbackAsync(string text, int caret, TextSpan spanToBeReplaced); internal delegate Task OpenCompletionWindowCallbackAsync(string text, int caret); +internal delegate Task CompletionWindowStateChangedCallbackAsync(string text, int caret); internal delegate Task> HighlightCallbackAsync(string text); internal delegate Task TransformKeyPressAsyncCallbackAsync(string text, int caret, KeyPress keyPress); internal delegate Task<(IReadOnlyList, int ArgumentIndex)> GetOverloadsCallbackAsync(string text, int caret); @@ -23,6 +24,8 @@ internal class TestPromptCallbacks : PromptCallbacks public SpanToReplaceByCompletionCallbackAsync? SpanToReplaceByCompletionCallback { get; set; } public CompletionCallbackAsync? CompletionCallback { get; set; } public OpenCompletionWindowCallbackAsync? OpenCompletionWindowCallback { get; set; } + public CompletionWindowStateChangedCallbackAsync? CompletionWindowOpenedCallback { get; set; } + public CompletionWindowStateChangedCallbackAsync? CompletionWindowClosedCallback { get; set; } public HighlightCallbackAsync? HighlightCallback { get; set; } public TransformKeyPressAsyncCallbackAsync? TransformKeyPressCallback { get; set; } public GetOverloadsCallbackAsync? GetOverloadsCallback { get; set; } @@ -58,6 +61,22 @@ OpenCompletionWindowCallback is null ? OpenCompletionWindowCallback(text, caret); } + protected override Task CompletionWindowOpenedAsync(string text, int caret, CancellationToken cancellationToken) + { + return + CompletionWindowOpenedCallback is null ? + base.CompletionWindowOpenedAsync(text, caret, cancellationToken) : + CompletionWindowOpenedCallback(text, caret); + } + + protected override Task CompletionWindowClosedAsync(string text, int caret, CancellationToken cancellationToken) + { + return + CompletionWindowClosedCallback is null ? + base.CompletionWindowClosedAsync(text, caret, cancellationToken) : + CompletionWindowClosedCallback(text, caret); + } + protected override Task> HighlightCallbackAsync(string text, CancellationToken cancellationToken) { return