Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/PrettyPrompt/Panes/CodePane.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ internal void Bind(CompletionPane completionPane, OverloadPane overloadPane)
this.overloadPane = overloadPane;
}

/// <summary>
/// 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.
/// </summary>
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;
Expand Down
22 changes: 15 additions & 7 deletions src/PrettyPrompt/Prompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private async Task<PromptResult> 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();
Expand Down Expand Up @@ -140,7 +140,8 @@ private async Task<PromptResult> 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);
Expand All @@ -153,7 +154,7 @@ private async Task<PromptResult> 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<bool> InterpretKeyPress(KeyPress key, CancellationToken cancellationToken)
{
if (!completionPane.WouldKeyPressCommitCompletionItem(key))
{
Expand All @@ -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)
/// <returns>True if the callback reformatted the document (the buffer was changed), else false.</returns>
private async Task<bool> 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);
Expand All @@ -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<PromptResult?> HandleKeyPressAction(CodePane codePane, KeyPress key, string inputText, CancellationToken cancellationToken)
Expand Down
6 changes: 4 additions & 2 deletions src/PrettyPrompt/Rendering/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ public void RenderOutput(
OverloadPane overloadPane,
CompletionPane completionPane,
IReadOnlyCollection<FormatSpan> 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;
Expand Down
39 changes: 39 additions & 0 deletions tests/PrettyPrompt.Tests/PromptTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,45 @@ public async Task ReadLine_WordWrap()
);
}

/// <summary>
/// Regression test for https://github.com/waf/CSharpRepl/issues/356.
/// When <see cref="PromptCallbacks.FormatInput"/> 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.
/// </summary>
[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()
{
Expand Down
10 changes: 10 additions & 0 deletions tests/PrettyPrompt.Tests/TestPromptCallbacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace PrettyPrompt.Tests;
internal delegate Task<IReadOnlyCollection<FormatSpan>> HighlightCallbackAsync(string text);
internal delegate Task<KeyPress> TransformKeyPressAsyncCallbackAsync(string text, int caret, KeyPress keyPress);
internal delegate Task<(IReadOnlyList<OverloadItem>, 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
{
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
}
}
Loading