diff --git a/src/PrettyPrompt/Panes/CodePane.cs b/src/PrettyPrompt/Panes/CodePane.cs
index 3ddc5ac..03a276b 100644
--- a/src/PrettyPrompt/Panes/CodePane.cs
+++ b/src/PrettyPrompt/Panes/CodePane.cs
@@ -163,6 +163,19 @@ internal void Bind(CompletionPane completionPane, OverloadPane overloadPane)
this.overloadPane = overloadPane;
}
+ ///
+ /// Re-capture the submit result's text from the current document, so the returned result matches
+ /// what's shown (and saved to history) after auto-formatting, not the pre-format snapshot.
+ ///
+ internal void RefreshSubmitResultText()
+ {
+ if (Result is not { IsSuccess: true })
+ {
+ return;
+ }
+ Result = new PromptResult(isSuccess: true, Document.GetText().EnvironmentNewlines(), Result.SubmitKeyInfo);
+ }
+
public async Task OnKeyDown(KeyPress key, CancellationToken cancellationToken)
{
if (key.Handled) return;
diff --git a/src/PrettyPrompt/Prompt.cs b/src/PrettyPrompt/Prompt.cs
index cc13119..e9e16ad 100644
--- a/src/PrettyPrompt/Prompt.cs
+++ b/src/PrettyPrompt/Prompt.cs
@@ -111,7 +111,7 @@ private async Task ReadLineCoreAsync()
// grab the code area width every key press, so we rerender appropriately when the console is resized.
codePane.MeasureConsole();
- await InterpretKeyPress(key, cancellationToken: default).ConfigureAwait(false);
+ var reformatted = await InterpretKeyPress(key, cancellationToken: default).ConfigureAwait(false);
// typing / word-wrapping may have scrolled the console, giving us more room.
codePane.MeasureConsole();
@@ -140,7 +140,8 @@ private async Task ReadLineCoreAsync()
break;
// user submitted the prompt, or a keybinding submitted the prompt
case PromptResult or KeyPressCallbackResult:
- await RenderSyntaxHighlightedOutput(renderer, codePane, overloadPane, completionPane, key, inputText, result).ConfigureAwait(false);
+ // a reformat on the submit keystroke must force a repaint (see Renderer.RenderOutput)
+ await RenderSyntaxHighlightedOutput(renderer, codePane, overloadPane, completionPane, key, inputText, result, forceRedrawOnSubmit: reformatted).ConfigureAwait(false);
//wait for potential previous saving
await (savePersistentHistoryTask ?? Task.CompletedTask).ConfigureAwait(false);
savePersistentHistoryTask = history.SavePersistentHistoryAsync(inputText);
@@ -153,7 +154,7 @@ private async Task ReadLineCoreAsync()
Debug.Assert(false, "Should never reach here due to infinite " + nameof(KeyPress.ReadForever));
return null;
- async Task InterpretKeyPress(KeyPress key, CancellationToken cancellationToken)
+ async Task InterpretKeyPress(KeyPress key, CancellationToken cancellationToken)
{
if (!completionPane.WouldKeyPressCommitCompletionItem(key))
{
@@ -166,16 +167,19 @@ async Task InterpretKeyPress(KeyPress key, CancellationToken cancellationToken)
foreach (var panes in keyPressHandlers)
await panes.OnKeyUp(key, cancellationToken).ConfigureAwait(false);
- await AutoFormatDocument(key, codePane, cancellationToken).ConfigureAwait(false);
+ var reformatted = await AutoFormatDocument(key, codePane, cancellationToken).ConfigureAwait(false);
//we don't support text selection while completion list is open
//text selection can put completion list into broken state, where filtering does not work
//so we want this assert to be true
Debug.Assert(!completionPane.IsOpen || (codePane.Selection is null));
+
+ return reformatted;
}
}
- private async Task AutoFormatDocument(KeyPress key, CodePane codePane, CancellationToken cancellationToken)
+ /// True if the callback reformatted the document (the buffer was changed), else false.
+ private async Task AutoFormatDocument(KeyPress key, CodePane codePane, CancellationToken cancellationToken)
{
var text = codePane.Document.GetText();
var (formattedText, newCaret) = await promptCallbacks.FormatInput(text, codePane.Document.Caret, key, cancellationToken).ConfigureAwait(false);
@@ -187,17 +191,21 @@ private async Task AutoFormatDocument(KeyPress key, CodePane codePane, Cancellat
if (formattedText[i] == '\r') ++removedChars;
}
codePane.Document.SetContents(codePane, formattedText.Replace("\r\n", "\n"), newCaret - removedChars);
+ // if this keystroke also submitted, the result text was snapshotted pre-format; re-capture it
+ codePane.RefreshSubmitResultText();
+ return true;
}
else
{
Debug.Assert(codePane.Document.Caret == newCaret);
+ return false;
}
}
- private async Task RenderSyntaxHighlightedOutput(Renderer renderer, CodePane codePane, OverloadPane overloadPane, CompletionPane completionPane, KeyPress key, string inputText, PromptResult? result)
+ private async Task RenderSyntaxHighlightedOutput(Renderer renderer, CodePane codePane, OverloadPane overloadPane, CompletionPane completionPane, KeyPress key, string inputText, PromptResult? result, bool forceRedrawOnSubmit = false)
{
var highlights = await highlighter.HighlightAsync(inputText, cancellationToken: default).ConfigureAwait(false);
- renderer.RenderOutput(result, codePane, overloadPane, completionPane, highlights, key);
+ renderer.RenderOutput(result, codePane, overloadPane, completionPane, highlights, key, forceRedrawOnSubmit);
}
private async Task HandleKeyPressAction(CodePane codePane, KeyPress key, string inputText, CancellationToken cancellationToken)
diff --git a/src/PrettyPrompt/Rendering/Renderer.cs b/src/PrettyPrompt/Rendering/Renderer.cs
index 04f442f..b802e5b 100644
--- a/src/PrettyPrompt/Rendering/Renderer.cs
+++ b/src/PrettyPrompt/Rendering/Renderer.cs
@@ -56,11 +56,13 @@ public void RenderOutput(
OverloadPane overloadPane,
CompletionPane completionPane,
IReadOnlyCollection highlights,
- KeyPress key)
+ KeyPress key,
+ bool forceRedraw = false)
{
if (result is not null)
{
- bool redraw = false;
+ // reformat-on-submit leaves the on-screen line stale, so repaint it. https://github.com/waf/CSharpRepl/issues/356
+ bool redraw = forceRedraw;
if (wasTextSelectedDuringPreviousRender && codePane.Selection is null)
{
redraw = true;
diff --git a/tests/PrettyPrompt.Tests/PromptTests.cs b/tests/PrettyPrompt.Tests/PromptTests.cs
index 081d713..52f3da5 100644
--- a/tests/PrettyPrompt.Tests/PromptTests.cs
+++ b/tests/PrettyPrompt.Tests/PromptTests.cs
@@ -111,6 +111,45 @@ public async Task ReadLine_WordWrap()
);
}
+ ///
+ /// Regression test for https://github.com/waf/CSharpRepl/issues/356.
+ /// When reformats the buffer on the very keystroke that
+ /// submits the prompt, the submit render path must force a redraw so the committed line shows the
+ /// formatted text. Otherwise the on-screen line is left showing the stale pre-format text, because
+ /// the reformat happened after the previous render and the submit path skips redrawing by default.
+ ///
+ [Fact]
+ public async Task ReadLine_FormatInputReformatsOnSubmit_RedrawsFormattedText()
+ {
+ var console = ConsoleStub.NewConsole();
+ console.StubInput($"hello{Enter}");
+
+ var prompt = new Prompt(
+ callbacks: new TestPromptCallbacks
+ {
+ // Only reformat on the submit keystroke (Enter), so the formatted text has never been
+ // rendered before submit - that's the scenario the fix addresses.
+ FormatInputCallback = (text, caret, keyPress) =>
+ Task.FromResult(
+ keyPress.ConsoleKeyInfo.Key == Enter
+ ? (text.ToUpperInvariant(), caret)
+ : (text, caret))
+ },
+ console: console);
+
+ var result = await prompt.ReadLineAsync();
+
+ Assert.True(result.IsSuccess);
+
+ // The returned result reflects the formatted text (re-captured after AutoFormatDocument), not the
+ // pre-format snapshot taken when the submit key was first handled.
+ Assert.Equal("HELLO", result.Text);
+
+ // ...and the final render repaints it; without the forced redraw the screen would still show the
+ // lowercase "hello" typed before the submit keystroke.
+ Assert.Contains("HELLO", console.GetFinalOutput());
+ }
+
[Fact]
public async Task ReadLine_HorizontalNavigationKeys()
{
diff --git a/tests/PrettyPrompt.Tests/TestPromptCallbacks.cs b/tests/PrettyPrompt.Tests/TestPromptCallbacks.cs
index 837fb94..25f0197 100644
--- a/tests/PrettyPrompt.Tests/TestPromptCallbacks.cs
+++ b/tests/PrettyPrompt.Tests/TestPromptCallbacks.cs
@@ -16,6 +16,7 @@ namespace PrettyPrompt.Tests;
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);
+internal delegate Task<(string Text, int Caret)> FormatInputCallbackAsync(string text, int caret, KeyPress keyPress);
internal class TestPromptCallbacks : PromptCallbacks
{
@@ -29,6 +30,7 @@ internal class TestPromptCallbacks : PromptCallbacks
public HighlightCallbackAsync? HighlightCallback { get; set; }
public TransformKeyPressAsyncCallbackAsync? TransformKeyPressCallback { get; set; }
public GetOverloadsCallbackAsync? GetOverloadsCallback { get; set; }
+ public FormatInputCallbackAsync? FormatInputCallback { get; set; }
public TestPromptCallbacks(params (KeyPressPattern Pattern, KeyPressCallbackAsync Callback)[]? keyPressCallbacks)
{
@@ -100,4 +102,12 @@ GetOverloadsCallback is null ?
base.GetOverloadsAsync(text, caret, cancellationToken) :
GetOverloadsCallback(text, caret);
}
+
+ protected override Task<(string Text, int Caret)> FormatInput(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken)
+ {
+ return
+ FormatInputCallback is null ?
+ base.FormatInput(text, caret, keyPress, cancellationToken) :
+ FormatInputCallback(text, caret, keyPress);
+ }
}
\ No newline at end of file