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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Release 6.0.5 (UNRELEASED)

- Fix the completion list offering items for a word that is no longer typed. The completion callback is now asked again whenever the typed word stops being an extension of the one it was asked about.

# Release 6.0.4

- Support "complex" completion items that rewrite a larger region of the document than the typed word (and reposition the caret) ([304](https://github.com/waf/PrettyPrompt/pull/304)).
Expand Down
23 changes: 17 additions & 6 deletions src/PrettyPrompt/Panes/CompletionPane.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ internal class CompletionPane : IKeyPressHandler
/// </summary>
private IReadOnlyList<CompletionItem> allCompletions = Array.Empty<CompletionItem>();

/// <summary>
/// The text of the span <see cref="allCompletions"/> was requested for. Callbacks are free to
/// return only the items matching that text, so the list stays usable exactly as long as the
/// user keeps extending it.
/// </summary>
private string allCompletionsFilterText = string.Empty;

/// <summary>
/// An "ordered view" over <see cref="allCompletions"/> that shows the list filtered by what the user has typed.
/// </summary>
Expand Down Expand Up @@ -71,6 +78,7 @@ private async Task Open(CancellationToken cancellationToken)
bool wasOpen = IsOpen;
this.IsOpen = true;
this.allCompletions = Array.Empty<CompletionItem>();
this.allCompletionsFilterText = string.Empty;
if (!wasOpen)
{
await promptCallbacks.CompletionWindowOpenedAsync(codePane.Document.GetText(), codePane.Document.Caret, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -224,17 +232,20 @@ await promptCallbacks.ShouldOpenCompletionWindowAsync(codePane.Document.GetText(
}
}

if (allCompletions.Count == 0)
// We ask the callback once and then only re-rank what it gave us, which is correct for
// as long as the user is refining the text we asked about. Deleting back past that text
// and typing something else leaves us ranking items for a word that is no longer there
// (ask on "G", delete it, type "H", and the G-items get sorted against "H"), so ask again.
if (allCompletions.Count == 0 ||
!documentText.AsSpan(spanToReplace).StartsWith(allCompletionsFilterText, StringComparison.Ordinal))
{
var completions = await promptCallbacks.GetCompletionItemsAsync(documentText, documentCaret, spanToReplace, cancellationToken).ConfigureAwait(false);
if (completions.Any())
{
allCompletions = completions;
if (completions.Any())
{
int height = Math.Min(codePane.CodeAreaHeight - VerticalPaddingHeight - overloadPane.GetCurrentHeight(), configuration.MaxCompletionItemsCount);
await FilteredView.UpdateItems(completions, documentText, documentCaret, spanToReplace, height, cancellationToken).ConfigureAwait(false);
}
allCompletionsFilterText = documentText.AsSpan(spanToReplace).ToString();
int height = Math.Min(codePane.CodeAreaHeight - VerticalPaddingHeight - overloadPane.GetCurrentHeight(), configuration.MaxCompletionItemsCount);
await FilteredView.UpdateItems(completions, documentText, documentCaret, spanToReplace, height, cancellationToken).ConfigureAwait(false);
}
else
{
Expand Down
36 changes: 36 additions & 0 deletions tests/PrettyPrompt.Tests/CompletionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,42 @@ public async Task ReadLine_RenderingBug()
Assert.Contains("bbb │ a vivid description of bbb", output[4]); //after press of 'b' (here was the bug)
}

/// <summary>
/// A callback that returns only the items matching the typed word -- what a real completion
/// provider does, and what <see cref="CompletionTestData"/> deliberately does not do.
/// </summary>
[Fact]
public async Task ReadLine_FilteringCallback_IsAskedAgainAfterTheTypedWordIsDeleted()
{
var console = ConsoleStub.NewConsole();
var prompt = new Prompt(
callbacks: new TestPromptCallbacks
{
CompletionCallback = (text, caret, spanToBeReplaced) =>
{
var typedWord = text.AsSpan(spanToBeReplaced).ToString();
return Task.FromResult<IReadOnlyList<CompletionItem>>(
new[] { "green", "grey", "hazel" }
.Where(c => c.StartsWith(typedWord, StringComparison.OrdinalIgnoreCase))
.Select(c => new CompletionItem(c))
.ToArray());
}
},
console: console);

console.StubInput(
$"gr", // the list is fetched for "gr", so it holds green and grey
$"{Backspace}{Backspace}", // and is stale from here on: neither is a completion of ""
$"h",
$"{Enter}", // commits the selected item
$"{Enter}");

var result = await prompt.ReadLineAsync();

Assert.True(result.IsSuccess);
Assert.Equal("hazel", result.Text);
}

public static Prompt ConfigurePrompt(IConsole console, PromptConfiguration? configuration = null, string[]? completions = null) =>
new(
callbacks: new TestPromptCallbacks
Expand Down
Loading