feat: add autocomplete foundation
- add reusable autocomplete provider contracts - integrate slash command completions with editor select list
This commit is contained in:
@@ -269,6 +269,38 @@ TinyTUI 现在已经具备最小可运行的 C# TUI 框架骨架:终端输入
|
||||
|
||||
参考:`tmp/tui/src/autocomplete.ts`、`tmp/tui/src/components/editor.ts`
|
||||
|
||||
本次增量已完成:
|
||||
|
||||
- 新增 `TinyTUI.Autocomplete` 模块,定义 `IAutocompleteProvider`、`AutocompleteRequest`、`AutocompleteSuggestions`、`AutocompleteApplyContext` 和 `AutocompleteApplyResult`,先把“查询候选”和“应用候选”从 Editor 中拆成可复用契约
|
||||
- 新增 `AutocompleteProviderBase`,提供默认的前缀替换实现,后续文件路径、变量、命令参数等 provider 可以只覆盖查询或特殊应用逻辑
|
||||
- 新增 `SlashCommandAutocompleteProvider` 和 `SlashCommand`,支持行首 `/` 命令名候选,确认后自动写回 `/{command} ` 并把光标放到参数位置
|
||||
- `Editor` 增加 `AutocompleteProvider`、`AutocompleteList`、`IsAutocompleteActive`、`StartAutocomplete`、`ConfirmAutocomplete`、`CancelAutocomplete` 和 `RenderAutocomplete`,补全列表复用现有 `SelectList` 的选择、滚动和渲染能力
|
||||
- 新增 `tui.autocomplete.trigger`、`tui.autocomplete.confirm`、`tui.autocomplete.cancel` 默认动作,Tab 触发或确认补全,Enter 在补全激活时确认,Escape 取消补全
|
||||
- 扩展 `TinyTUI.ComponentChecks` 覆盖 slash command 候选渲染、方向键切换候选、Tab 确认应用和 Escape 取消
|
||||
|
||||
这样设计的原因:
|
||||
|
||||
- provider 契约返回 `items + prefix`,与 `tmp/tui/src/autocomplete.ts` 的核心模型一致,Editor 不需要知道候选来自命令、路径还是特殊前缀
|
||||
- 应用候选独立成 `ApplyCompletion`,是为了保留 slash command 自动补空格、路径目录不补空格、引号去重等后续差异,不把这些规则硬塞进 Editor
|
||||
- Editor 内部持有 `SelectList`,先把补全选择和确认流程跑通,后续 overlay 只需要挂载 `RenderAutocomplete` 或直接挂载同一个列表组件
|
||||
- 补全确认通过现有 `Edit` 包装写回文本,所以撤销栈、redo 清理和 `OnChanged` 行为与普通编辑保持一致
|
||||
|
||||
怎么看懂当前代码:
|
||||
|
||||
- 先看 `Autocomplete/IAutocompleteProvider.cs`,它定义自动补全框架的两个关键动作:查询候选和应用候选
|
||||
- 再看 `Autocomplete/SlashCommandAutocompleteProvider.cs`,它展示了一个最小 provider 如何根据当前行和光标返回候选,以及如何把候选写回文本
|
||||
- 最后看 `Components/Editor/Editor.Autocomplete.cs`,这里是 Editor 的补全状态机:启动查询、用 `SelectList` 展示候选、确认应用、取消清理
|
||||
- `Components/Editor/Editor.cs` 的 `HandleInput` 入口可以看到补全输入优先级:活动补全先消费导航和确认,普通 Tab 强制触发补全,普通文本输入后尝试自然补全
|
||||
|
||||
对比 `tmp/tui` 后续仍需补齐:
|
||||
|
||||
- C# 侧 provider 契约已用 `ValueTask` 支持异步形态,但 Editor 当前 `HandleInput` 仍是同步接口,会阻塞等待 provider;后续需要 Runtime 级 invalidate 和取消令牌来实现真正非阻塞异步补全
|
||||
- 还没有移植文件路径 provider、`@` 附件前缀、引号路径、`~/` 展开、目录优先排序和 fd fuzzy 搜索
|
||||
- 还没有实现 slash command 参数补全,参考实现里的 `getArgumentCompletions` 需要在 C# 侧扩展到 `SlashCommand`
|
||||
- 还没有把补全列表通过 `OverlayManager` 自动挂到光标附近,目前只提供 `RenderAutocomplete` 和内部 `SelectList` 供后续 overlay 接线
|
||||
- 还没有做补全预览文本、候选变化 debounce、过期请求丢弃和 AbortController 等请求竞争处理
|
||||
- 当前默认前缀替换基类按字符串长度处理 prefix,后续支持宽字符或跨行补全时需要把 provider 光标协议统一成 Rune 列或专门的文本范围类型
|
||||
|
||||
### 8. 图像和特殊终端内容
|
||||
|
||||
目标:为 Kitty / iTerm2 图像和特殊终端序列预留完整框架入口
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 描述应用一个补全候选时的编辑器上下文
|
||||
/// </summary>
|
||||
public sealed record AutocompleteApplyContext(
|
||||
IReadOnlyList<string> Lines,
|
||||
int CursorLine,
|
||||
int CursorColumn,
|
||||
AutocompleteItem Item,
|
||||
string Prefix);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 表示补全候选应用后的编辑器文本和光标位置
|
||||
/// </summary>
|
||||
public sealed record AutocompleteApplyResult(IReadOnlyList<string> Lines, int CursorLine, int CursorColumn);
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 表示自动补全列表中的一个候选项
|
||||
/// </summary>
|
||||
public sealed record AutocompleteItem(string Value, string Label, string? Description = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用同一段文本创建值和标签一致的候选项
|
||||
/// </summary>
|
||||
public static AutocompleteItem FromText(string text) => new(text, text);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 提供通用前缀替换逻辑的自动补全 provider 基类
|
||||
/// </summary>
|
||||
public abstract class AutocompleteProviderBase : IAutocompleteProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask<AutocompleteSuggestions?> GetSuggestionsAsync(AutocompleteRequest request);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual AutocompleteApplyResult ApplyCompletion(AutocompleteApplyContext context)
|
||||
{
|
||||
var lines = context.Lines.ToArray();
|
||||
if (context.CursorLine < 0 || context.CursorLine >= lines.Length)
|
||||
return new AutocompleteApplyResult(lines, context.CursorLine, context.CursorColumn);
|
||||
|
||||
var line = lines[context.CursorLine];
|
||||
var prefixStart = Math.Clamp(context.CursorColumn - context.Prefix.Length, 0, line.Length);
|
||||
var cursor = Math.Clamp(context.CursorColumn, 0, line.Length);
|
||||
lines[context.CursorLine] = line[..prefixStart] + context.Item.Value + line[cursor..];
|
||||
|
||||
return new AutocompleteApplyResult(
|
||||
lines,
|
||||
context.CursorLine,
|
||||
prefixStart + context.Item.Value.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 描述一次自动补全查询所需的编辑器上下文
|
||||
/// </summary>
|
||||
public sealed record AutocompleteRequest(
|
||||
IReadOnlyList<string> Lines,
|
||||
int CursorLine,
|
||||
int CursorColumn,
|
||||
bool Force,
|
||||
CancellationToken CancellationToken = default);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 表示 provider 返回的自动补全候选和需要替换的前缀
|
||||
/// </summary>
|
||||
public sealed record AutocompleteSuggestions(IReadOnlyList<AutocompleteItem> Items, string Prefix);
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 为编辑器或命令输入提供自动补全候选
|
||||
/// </summary>
|
||||
public interface IAutocompleteProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前光标位置可用的候选项
|
||||
/// </summary>
|
||||
ValueTask<AutocompleteSuggestions?> GetSuggestionsAsync(AutocompleteRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 将用户选中的候选项应用到文本模型
|
||||
/// </summary>
|
||||
AutocompleteApplyResult ApplyCompletion(AutocompleteApplyContext context);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 描述一个可由斜杠命令补全的命令
|
||||
/// </summary>
|
||||
public sealed record SlashCommand(string Name, string? Description = null, string? ArgumentHint = null);
|
||||
@@ -0,0 +1,66 @@
|
||||
namespace TinyTUI.Autocomplete;
|
||||
|
||||
/// <summary>
|
||||
/// 为行首斜杠命令提供候选项
|
||||
/// </summary>
|
||||
public sealed class SlashCommandAutocompleteProvider(IEnumerable<SlashCommand> commands) : AutocompleteProviderBase
|
||||
{
|
||||
private readonly IReadOnlyList<SlashCommand> _commands = [.. commands];
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask<AutocompleteSuggestions?> GetSuggestionsAsync(AutocompleteRequest request)
|
||||
{
|
||||
if (request.CursorLine < 0 || request.CursorLine >= request.Lines.Count)
|
||||
return ValueTask.FromResult<AutocompleteSuggestions?>(null);
|
||||
|
||||
var line = request.Lines[request.CursorLine];
|
||||
var cursor = Math.Clamp(request.CursorColumn, 0, line.Length);
|
||||
var beforeCursor = line[..cursor];
|
||||
if (!beforeCursor.StartsWith('/'))
|
||||
return ValueTask.FromResult<AutocompleteSuggestions?>(null);
|
||||
|
||||
// 当前增量只处理命令名补全 命令参数补全后续由组合 provider 扩展
|
||||
if (beforeCursor.Contains(' ', StringComparison.Ordinal))
|
||||
return ValueTask.FromResult<AutocompleteSuggestions?>(null);
|
||||
|
||||
var query = beforeCursor[1..];
|
||||
var items = _commands
|
||||
.Where(command => command.Name.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(command => command.Name.StartsWith(query, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
|
||||
.ThenBy(command => command.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(command => new AutocompleteItem(command.Name, command.Name, BuildDescription(command)))
|
||||
.ToArray();
|
||||
|
||||
return ValueTask.FromResult<AutocompleteSuggestions?>(
|
||||
items.Length == 0 ? null : new AutocompleteSuggestions(items, beforeCursor));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override AutocompleteApplyResult ApplyCompletion(AutocompleteApplyContext context)
|
||||
{
|
||||
var lines = context.Lines.ToArray();
|
||||
if (context.CursorLine < 0 || context.CursorLine >= lines.Length)
|
||||
return new AutocompleteApplyResult(lines, context.CursorLine, context.CursorColumn);
|
||||
|
||||
var line = lines[context.CursorLine];
|
||||
var prefixStart = Math.Clamp(context.CursorColumn - context.Prefix.Length, 0, line.Length);
|
||||
var cursor = Math.Clamp(context.CursorColumn, 0, line.Length);
|
||||
var replacement = "/" + context.Item.Value + " ";
|
||||
lines[context.CursorLine] = line[..prefixStart] + replacement + line[cursor..];
|
||||
|
||||
return new AutocompleteApplyResult(lines, context.CursorLine, prefixStart + replacement.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并参数提示和描述用于 SelectList 的说明列
|
||||
/// </summary>
|
||||
private static string? BuildDescription(SlashCommand command)
|
||||
{
|
||||
if (command.ArgumentHint is null)
|
||||
return command.Description;
|
||||
|
||||
return command.Description is null
|
||||
? command.ArgumentHint
|
||||
: $"{command.ArgumentHint} - {command.Description}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using TinyTUI.Autocomplete;
|
||||
using TinyTUI.Input;
|
||||
|
||||
namespace TinyTUI.Components;
|
||||
|
||||
public sealed partial class Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置当前编辑器使用的自动补全 provider
|
||||
/// </summary>
|
||||
public IAutocompleteProvider? AutocompleteProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否正在显示补全候选
|
||||
/// </summary>
|
||||
public bool IsAutocompleteActive => _autocompleteSuggestions is not null;
|
||||
|
||||
private AutocompleteSuggestions? _autocompleteSuggestions;
|
||||
|
||||
/// <summary>
|
||||
/// 渲染当前补全候选列表供 overlay 或外部布局挂载
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> RenderAutocomplete(int width)
|
||||
=> IsAutocompleteActive ? AutocompleteList.Render(width) : [];
|
||||
|
||||
/// <summary>
|
||||
/// 触发补全查询并刷新内部 SelectList
|
||||
/// </summary>
|
||||
public bool StartAutocomplete(bool force = false)
|
||||
{
|
||||
if (AutocompleteProvider is not { } provider)
|
||||
return false;
|
||||
|
||||
var request = new AutocompleteRequest([.. _lines], _cursorRow, _cursorColumn, force);
|
||||
var suggestions = provider.GetSuggestionsAsync(request).AsTask().GetAwaiter().GetResult();
|
||||
if (suggestions is null || suggestions.Items.Count == 0)
|
||||
{
|
||||
CancelAutocomplete();
|
||||
return false;
|
||||
}
|
||||
|
||||
_autocompleteSuggestions = suggestions;
|
||||
AutocompleteList.SetItems(suggestions.Items.Select(static item => new SelectItem(item.Value, item.Label, item.Description)));
|
||||
AutocompleteList.SetFilter(string.Empty);
|
||||
AutocompleteList.SetSelectedIndex(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消当前补全状态
|
||||
/// </summary>
|
||||
public void CancelAutocomplete()
|
||||
{
|
||||
_autocompleteSuggestions = null;
|
||||
AutocompleteList.SetItems(Array.Empty<SelectItem>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前选中的补全候选应用到编辑器文本
|
||||
/// </summary>
|
||||
public bool ConfirmAutocomplete()
|
||||
{
|
||||
if (_autocompleteSuggestions is not { } suggestions ||
|
||||
AutocompleteProvider is not { } provider ||
|
||||
AutocompleteList.SelectedItemModel is not { } selected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var item = new AutocompleteItem(selected.Value, selected.Label, selected.Description);
|
||||
var context = new AutocompleteApplyContext([.. _lines], _cursorRow, _cursorColumn, item, suggestions.Prefix);
|
||||
|
||||
Edit(() =>
|
||||
{
|
||||
var result = provider.ApplyCompletion(context);
|
||||
ApplyAutocompleteResult(result);
|
||||
});
|
||||
|
||||
CancelAutocomplete();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把补全结果写回编辑器内部文本模型并约束光标位置
|
||||
/// </summary>
|
||||
private void ApplyAutocompleteResult(AutocompleteApplyResult result)
|
||||
{
|
||||
_lines.Clear();
|
||||
_lines.AddRange(result.Lines.Count == 0 ? [string.Empty] : result.Lines);
|
||||
|
||||
_cursorRow = Math.Clamp(result.CursorLine, 0, _lines.Count - 1);
|
||||
_cursorColumn = Math.Clamp(result.CursorColumn, 0, GetRuneCount(_lines[_cursorRow]));
|
||||
_preferredVisualColumn = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 活动补全优先消费选择和确认相关快捷键
|
||||
/// </summary>
|
||||
private bool TryHandleAutocompleteInput(TuiInputEvent input)
|
||||
{
|
||||
if (!IsAutocompleteActive)
|
||||
return false;
|
||||
|
||||
if (_keybindings.Matches(input, TuiKeybindings.AutocompleteCancel))
|
||||
{
|
||||
CancelAutocomplete();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_keybindings.Matches(input, TuiKeybindings.AutocompleteConfirm))
|
||||
return ConfirmAutocomplete();
|
||||
|
||||
// 候选列表继续复用 SelectList 的导航逻辑 保持滚动和选择事件一致
|
||||
if (_keybindings.Matches(input, TuiKeybindings.SelectUp) ||
|
||||
_keybindings.Matches(input, TuiKeybindings.SelectDown) ||
|
||||
_keybindings.Matches(input, TuiKeybindings.SelectHome) ||
|
||||
_keybindings.Matches(input, TuiKeybindings.SelectEnd))
|
||||
{
|
||||
AutocompleteList.HandleInput(input);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ namespace TinyTUI.Components;
|
||||
/// <summary>
|
||||
/// 支持多行文本编辑的基础编辑器组件
|
||||
/// </summary>
|
||||
public sealed partial class Editor(ITextMeasurer? textMeasurer = null, KeybindingRegistry? keybindings = null) : IInputComponent, IFocusableComponent
|
||||
public sealed partial class Editor : IInputComponent, IFocusableComponent
|
||||
{
|
||||
private readonly ITextMeasurer _textMeasurer = textMeasurer ?? new TerminalTextMeasurer();
|
||||
private readonly KeybindingRegistry _keybindings = keybindings ?? KeybindingRegistry.CreateDefault();
|
||||
private readonly ITextMeasurer _textMeasurer;
|
||||
private readonly KeybindingRegistry _keybindings;
|
||||
private readonly List<string> _lines = [string.Empty];
|
||||
private readonly Stack<EditorSnapshot> _undoStack = [];
|
||||
private readonly Stack<EditorSnapshot> _redoStack = [];
|
||||
@@ -23,6 +23,20 @@ public sealed partial class Editor(ITextMeasurer? textMeasurer = null, Keybindin
|
||||
private int _scrollOffset;
|
||||
private int? _preferredVisualColumn;
|
||||
|
||||
/// <summary>
|
||||
/// 创建多行编辑器组件
|
||||
/// </summary>
|
||||
public Editor(ITextMeasurer? textMeasurer = null, KeybindingRegistry? keybindings = null)
|
||||
{
|
||||
_textMeasurer = textMeasurer ?? new TerminalTextMeasurer();
|
||||
_keybindings = keybindings ?? KeybindingRegistry.CreateDefault();
|
||||
AutocompleteList = new SelectList(_textMeasurer, _keybindings)
|
||||
{
|
||||
Height = 5,
|
||||
Focused = true,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置编辑器高度
|
||||
/// </summary>
|
||||
@@ -41,6 +55,11 @@ public sealed partial class Editor(ITextMeasurer? textMeasurer = null, Keybindin
|
||||
/// <inheritdoc />
|
||||
public bool Focused { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取编辑器内部复用的补全列表组件
|
||||
/// </summary>
|
||||
public SelectList AutocompleteList { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前编辑器文本
|
||||
/// </summary>
|
||||
@@ -68,15 +87,26 @@ public sealed partial class Editor(ITextMeasurer? textMeasurer = null, Keybindin
|
||||
/// <inheritdoc />
|
||||
public void HandleInput(TuiInputEvent input)
|
||||
{
|
||||
if (TryHandleAutocompleteInput(input))
|
||||
return;
|
||||
|
||||
switch (input)
|
||||
{
|
||||
case { Kind: TuiInputEventKind.Text }:
|
||||
Edit(() => InsertText(input.Value));
|
||||
StartAutocomplete(force: false);
|
||||
break;
|
||||
case { Kind: TuiInputEventKind.Paste }:
|
||||
Edit(() => InsertText(input.Value.Replace("\r\n", "\n")));
|
||||
CancelAutocomplete();
|
||||
break;
|
||||
case { Kind: TuiInputEventKind.Key }:
|
||||
if (_keybindings.Matches(input, TuiKeybindings.AutocompleteTrigger))
|
||||
{
|
||||
StartAutocomplete(force: true);
|
||||
return;
|
||||
}
|
||||
|
||||
HandleKey(input);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ public static class TuiKeybindings
|
||||
public const string EditorClear = "tui.editor.clear";
|
||||
public const string EditorNewLine = "tui.editor.newLine";
|
||||
public const string EditorSubmit = "tui.editor.submit";
|
||||
public const string AutocompleteTrigger = "tui.autocomplete.trigger";
|
||||
public const string AutocompleteConfirm = "tui.autocomplete.confirm";
|
||||
public const string AutocompleteCancel = "tui.autocomplete.cancel";
|
||||
public const string InputNewLine = "tui.input.newLine";
|
||||
public const string InputSubmit = "tui.input.submit";
|
||||
public const string InputCancel = "tui.input.cancel";
|
||||
@@ -54,6 +57,9 @@ public static class TuiKeybindings
|
||||
Define(EditorClear, [KeyNames.Ctrl("c")], "Clear current input"),
|
||||
Define(EditorNewLine, [KeyNames.Enter], "Insert editor newline"),
|
||||
Define(EditorSubmit, [KeyNames.Ctrl(KeyNames.Enter), KeyNames.Ctrl("s")], "Submit editor input"),
|
||||
Define(AutocompleteTrigger, [KeyNames.Tab], "Trigger autocomplete"),
|
||||
Define(AutocompleteConfirm, [KeyNames.Tab, KeyNames.Enter], "Confirm autocomplete"),
|
||||
Define(AutocompleteCancel, [KeyNames.Escape], "Cancel autocomplete"),
|
||||
Define(InputNewLine, [KeyNames.Shift(KeyNames.Enter)], "Insert newline"),
|
||||
Define(InputSubmit, [KeyNames.Enter], "Submit input"),
|
||||
Define(InputCancel, [KeyNames.Escape], "Cancel input"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using TinyTUI.Components;
|
||||
using TinyTUI.Autocomplete;
|
||||
using TinyTUI.Input;
|
||||
using TinyTUI.Rendering;
|
||||
using TinyTUI.Text;
|
||||
@@ -41,6 +42,29 @@ loader.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape));
|
||||
AssertTrue(loader.IsCanceled, "cancellable loader token");
|
||||
AssertTrue(canceled, "cancellable loader callback");
|
||||
|
||||
var autocompleteEditor = new Editor(measurer)
|
||||
{
|
||||
AutocompleteProvider = new SlashCommandAutocompleteProvider(
|
||||
[
|
||||
new SlashCommand("help", "show help"),
|
||||
new SlashCommand("history", "show history"),
|
||||
]),
|
||||
};
|
||||
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Text, "/h"));
|
||||
AssertTrue(autocompleteEditor.IsAutocompleteActive, "slash command autocomplete active");
|
||||
AssertTrue(autocompleteEditor.RenderAutocomplete(40).Any(line => line.Contains("help", StringComparison.Ordinal)), "slash command autocomplete render");
|
||||
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Down));
|
||||
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Tab));
|
||||
AssertEqual("/history ", autocompleteEditor.Value, "slash command autocomplete confirm");
|
||||
AssertFalse(autocompleteEditor.IsAutocompleteActive, "slash command autocomplete closes after confirm");
|
||||
|
||||
autocompleteEditor.Value = "/h";
|
||||
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Tab));
|
||||
AssertTrue(autocompleteEditor.IsAutocompleteActive, "forced autocomplete active");
|
||||
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape));
|
||||
AssertFalse(autocompleteEditor.IsAutocompleteActive, "autocomplete cancel");
|
||||
AssertEqual("/h", autocompleteEditor.Value, "autocomplete cancel keeps text");
|
||||
|
||||
Console.WriteLine("TinyTUI component checks passed");
|
||||
|
||||
static void AssertEqual<T>(T expected, T actual, string name)
|
||||
|
||||
Reference in New Issue
Block a user