602e271f6c
- support argument completion callbacks on slash commands - apply selected argument suggestions without rewriting command names - add autocomplete provider regression tests
281 lines
11 KiB
C#
281 lines
11 KiB
C#
using TinyTUI.Autocomplete;
|
|
|
|
namespace TinyTUI.Tests.Autocomplete;
|
|
|
|
public sealed class AutocompleteProviderTests : IDisposable
|
|
{
|
|
private readonly string _basePath = Path.Combine(Path.GetTempPath(), "ttui-autocomplete-" + Guid.NewGuid().ToString("N"));
|
|
|
|
public AutocompleteProviderTests()
|
|
{
|
|
Directory.CreateDirectory(_basePath);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderExtractsForcedPathPrefixes()
|
|
{
|
|
WriteFile("alpha.txt");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
|
|
var dotSlash = await GetSuggestions(provider, "./a", force: true);
|
|
var parent = await GetSuggestions(provider, "../", force: true);
|
|
var slash = await GetSuggestions(provider, "/", force: true);
|
|
|
|
Assert.Equal("./a", dotSlash?.Prefix);
|
|
Assert.Equal("../", parent?.Prefix);
|
|
Assert.Equal("/", slash?.Prefix);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderReturnsDirectoriesFirstThenFiles()
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(_basePath, "src"));
|
|
WriteFile("src.txt");
|
|
WriteFile("alpha.txt");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
|
|
var result = await GetSuggestions(provider, "s", force: true);
|
|
Assert.NotNull(result);
|
|
var values = result.Items.Select(static item => item.Value).ToArray();
|
|
|
|
Assert.Equal(["src/", "src.txt"], values);
|
|
Assert.Equal("src/", result.Items[0].Label);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderPreservesPathPrefixForNestedCompletion()
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(_basePath, "src"));
|
|
WriteFile("src/index.cs");
|
|
WriteFile("src/input.cs");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
|
|
var result = await GetSuggestions(provider, "./src/i", force: true);
|
|
Assert.NotNull(result);
|
|
var values = result.Items.Select(static item => item.Value).ToArray();
|
|
|
|
Assert.Equal("./src/i", result.Prefix);
|
|
Assert.Equal(["./src/index.cs", "./src/input.cs"], values);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderCompletesAttachmentPrefix()
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(_basePath, "src"));
|
|
WriteFile("src.txt");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
|
|
var result = await GetSuggestions(provider, "@src");
|
|
Assert.NotNull(result);
|
|
var values = result.Items.Select(static item => item.Value).ToArray();
|
|
|
|
Assert.Equal("@src", result.Prefix);
|
|
Assert.Equal(["@src/", "@src.txt"], values);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderQuotesPathsWithSpaces()
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(_basePath, "my folder"));
|
|
WriteFile("my folder/test.txt");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
|
|
var regular = await GetSuggestions(provider, "my", force: true);
|
|
var attachment = await GetSuggestions(provider, "@my");
|
|
|
|
Assert.Contains("\"my folder/\"", regular?.Items.Select(static item => item.Value) ?? []);
|
|
Assert.Contains("@\"my folder/\"", attachment?.Items.Select(static item => item.Value) ?? []);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderContinuesInsideQuotedAttachmentPath()
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(_basePath, "my folder"));
|
|
WriteFile("my folder/other.txt");
|
|
WriteFile("my folder/test.txt");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
const string line = "@\"my folder/\"";
|
|
|
|
var result = await GetSuggestions(provider, line, cursorColumn: line.Length - 1);
|
|
Assert.NotNull(result);
|
|
var values = result.Items.Select(static item => item.Value).ToArray();
|
|
|
|
Assert.Equal("@\"my folder/", result.Prefix);
|
|
Assert.Equal(["@\"my folder/other.txt\"", "@\"my folder/test.txt\""], values);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderAppliesDirectoryAndFileCompletions()
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(_basePath, "src"));
|
|
WriteFile("src.txt");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
var result = await GetSuggestions(provider, "@src");
|
|
|
|
var directory = provider.ApplyCompletion(new AutocompleteApplyContext(["see @src"], 0, "see @src".Length, result!.Items[0], result.Prefix));
|
|
var file = provider.ApplyCompletion(new AutocompleteApplyContext(["see @src"], 0, "see @src".Length, result.Items[1], result.Prefix));
|
|
|
|
Assert.Equal("see @src/", directory.Lines[0]);
|
|
Assert.Equal("see @src.txt ", file.Lines[0]);
|
|
Assert.Equal("see @src/".Length, directory.CursorColumn);
|
|
Assert.Equal("see @src.txt ".Length, file.CursorColumn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderAppliesQuotedCompletionWithoutDuplicatingClosingQuote()
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(_basePath, "my folder"));
|
|
WriteFile("my folder/test.txt");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
const string line = "@\"my folder/te\"";
|
|
var result = await GetSuggestions(provider, line, cursorColumn: line.Length - 1);
|
|
var item = Assert.Single(result!.Items);
|
|
|
|
var applied = provider.ApplyCompletion(new AutocompleteApplyContext([line], 0, line.Length - 1, item, result.Prefix));
|
|
|
|
Assert.Equal("@\"my folder/test.txt\" ", applied.Lines[0]);
|
|
Assert.Equal("@\"my folder/test.txt\" ".Length, applied.CursorColumn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FilePathProviderAppliesRegularQuotedCompletionWithoutDuplicatingClosingQuote()
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(_basePath, "my folder"));
|
|
WriteFile("my folder/test.txt");
|
|
var provider = new FilePathAutocompleteProvider(_basePath);
|
|
const string line = "\"my folder/te\"";
|
|
var result = await GetSuggestions(provider, line, force: true, cursorColumn: line.Length - 1);
|
|
var item = Assert.Single(result!.Items);
|
|
|
|
var applied = provider.ApplyCompletion(new AutocompleteApplyContext([line], 0, line.Length - 1, item, result.Prefix));
|
|
|
|
Assert.Equal("\"my folder/test.txt\" ", applied.Lines[0]);
|
|
Assert.Equal("\"my folder/test.txt\" ".Length, applied.CursorColumn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompositeProviderUsesFirstProviderWithSuggestions()
|
|
{
|
|
WriteFile("src.txt");
|
|
var provider = new CompositeAutocompleteProvider(
|
|
[
|
|
new SlashCommandAutocompleteProvider([new SlashCommand("help")]),
|
|
new FilePathAutocompleteProvider(_basePath),
|
|
]);
|
|
|
|
var command = await GetSuggestions(provider, "/h");
|
|
var path = await GetSuggestions(provider, "src", force: true);
|
|
|
|
Assert.Equal("/h", command?.Prefix);
|
|
Assert.Equal("help", command?.Items.Single().Value);
|
|
Assert.Equal("src", path?.Prefix);
|
|
Assert.Equal("src.txt", path?.Items.Single().Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SlashCommandProviderReturnsArgumentCompletions()
|
|
{
|
|
var provider = new SlashCommandAutocompleteProvider(
|
|
[
|
|
new SlashCommand(
|
|
"model",
|
|
ArgumentCompletionProvider: (prefix, _) => ValueTask.FromResult<IReadOnlyList<AutocompleteItem>?>(
|
|
[new AutocompleteItem("gpt-5", "gpt-5", $"prefix:{prefix}")]))
|
|
]);
|
|
|
|
var result = await GetSuggestions(provider, "/model gp");
|
|
|
|
Assert.NotNull(result);
|
|
var item = Assert.Single(result.Items);
|
|
Assert.Equal("gp", result.Prefix);
|
|
Assert.Equal("gpt-5", item.Value);
|
|
Assert.Equal("prefix:gp", item.Description);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SlashCommandProviderAppliesArgumentCompletionWithoutRewritingCommandName()
|
|
{
|
|
var provider = new SlashCommandAutocompleteProvider(
|
|
[
|
|
new SlashCommand(
|
|
"model",
|
|
ArgumentCompletionProvider: static (_, _) => ValueTask.FromResult<IReadOnlyList<AutocompleteItem>?>(
|
|
[AutocompleteItem.FromText("gpt-5")]))
|
|
]);
|
|
const string line = "/model gp";
|
|
var result = await GetSuggestions(provider, line);
|
|
var item = Assert.Single(result!.Items);
|
|
|
|
var applied = provider.ApplyCompletion(new AutocompleteApplyContext([line], 0, line.Length, item, result.Prefix));
|
|
|
|
Assert.Equal("/model gpt-5", applied.Lines[0]);
|
|
Assert.Equal("/model gpt-5".Length, applied.CursorColumn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SlashCommandProviderDoesNotRewriteCommandNameWhenArgumentPrefixIsEmpty()
|
|
{
|
|
var provider = new SlashCommandAutocompleteProvider(
|
|
[
|
|
new SlashCommand(
|
|
"model",
|
|
ArgumentCompletionProvider: static (_, _) => ValueTask.FromResult<IReadOnlyList<AutocompleteItem>?>(
|
|
[AutocompleteItem.FromText("gpt-5")]))
|
|
]);
|
|
const string line = "/model ";
|
|
var result = await GetSuggestions(provider, line);
|
|
var item = Assert.Single(result!.Items);
|
|
|
|
var applied = provider.ApplyCompletion(new AutocompleteApplyContext([line], 0, line.Length, item, result.Prefix));
|
|
|
|
Assert.Equal("/model gpt-5", applied.Lines[0]);
|
|
Assert.Equal("/model gpt-5".Length, applied.CursorColumn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SlashCommandProviderReturnsNoArgumentSuggestionsWithoutCallback()
|
|
{
|
|
var provider = new SlashCommandAutocompleteProvider([new SlashCommand("model")]);
|
|
|
|
var result = await GetSuggestions(provider, "/model gp");
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SlashCommandProviderKeepsCommandNameCompletion()
|
|
{
|
|
var provider = new SlashCommandAutocompleteProvider(
|
|
[
|
|
new SlashCommand("help", "show help"),
|
|
new SlashCommand("history", "show history"),
|
|
]);
|
|
|
|
var result = await GetSuggestions(provider, "/h");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("/h", result.Prefix);
|
|
Assert.Equal(["help", "history"], result.Items.Select(static item => item.Value).ToArray());
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_basePath))
|
|
Directory.Delete(_basePath, true);
|
|
}
|
|
|
|
private async Task<AutocompleteSuggestions?> GetSuggestions(
|
|
IAutocompleteProvider provider,
|
|
string line,
|
|
bool force = false,
|
|
int? cursorColumn = null)
|
|
=> await provider.GetSuggestionsAsync(new AutocompleteRequest([line], 0, cursorColumn ?? line.Length, force));
|
|
|
|
private void WriteFile(string relativePath)
|
|
{
|
|
var path = Path.Combine(_basePath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
File.WriteAllText(path, string.Empty);
|
|
}
|
|
}
|