155 lines
6 KiB
C#
155 lines
6 KiB
C#
using Spectre.Console;
|
|
|
|
namespace NozCli.Repl;
|
|
|
|
/// Minimal readline with history + Tab-completion.
|
|
public class ReplInput
|
|
{
|
|
private readonly List<string> _history = [];
|
|
private int _histIdx = -1;
|
|
private readonly CommandRegistry _registry;
|
|
private ReplContext? _ctx;
|
|
|
|
public ReplInput(CommandRegistry registry) => _registry = registry;
|
|
|
|
public void SetContext(ReplContext ctx) => _ctx = ctx;
|
|
|
|
public string? Read(string prompt)
|
|
{
|
|
AnsiConsole.Markup(prompt);
|
|
|
|
var buffer = new List<char>();
|
|
int cursor = 0;
|
|
string? tab = null; // last Tab suggestion
|
|
int tabIdx = 0;
|
|
|
|
while (true)
|
|
{
|
|
var key = Console.ReadKey(intercept: true);
|
|
|
|
// ── Enter ─────────────────────────────────────────────────
|
|
if (key.Key == ConsoleKey.Enter)
|
|
{
|
|
Console.WriteLine();
|
|
var result = new string(buffer.ToArray());
|
|
if (!string.IsNullOrWhiteSpace(result))
|
|
{
|
|
_history.Add(result);
|
|
_histIdx = -1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ── Ctrl-C / Ctrl-D ───────────────────────────────────────
|
|
if (key.Key == ConsoleKey.C && key.Modifiers.HasFlag(ConsoleModifiers.Control))
|
|
{
|
|
Console.WriteLine();
|
|
return null;
|
|
}
|
|
|
|
// ── Backspace ─────────────────────────────────────────────
|
|
if (key.Key == ConsoleKey.Backspace)
|
|
{
|
|
if (cursor > 0)
|
|
{
|
|
buffer.RemoveAt(cursor - 1);
|
|
cursor--;
|
|
RedrawLine(prompt, buffer, cursor);
|
|
}
|
|
tab = null;
|
|
continue;
|
|
}
|
|
|
|
// ── Arrow up/down — history ───────────────────────────────
|
|
if (key.Key == ConsoleKey.UpArrow && _history.Count > 0)
|
|
{
|
|
_histIdx = Math.Clamp(_histIdx < 0 ? _history.Count - 1 : _histIdx - 1, 0, _history.Count - 1);
|
|
buffer = [.. _history[_histIdx]];
|
|
cursor = buffer.Count;
|
|
RedrawLine(prompt, buffer, cursor);
|
|
continue;
|
|
}
|
|
if (key.Key == ConsoleKey.DownArrow)
|
|
{
|
|
if (_histIdx >= 0 && _histIdx < _history.Count - 1)
|
|
{
|
|
_histIdx++;
|
|
buffer = [.. _history[_histIdx]];
|
|
}
|
|
else { _histIdx = -1; buffer.Clear(); }
|
|
cursor = buffer.Count;
|
|
RedrawLine(prompt, buffer, cursor);
|
|
continue;
|
|
}
|
|
|
|
// ── Arrow left/right — cursor move ────────────────────────
|
|
if (key.Key == ConsoleKey.LeftArrow && cursor > 0) { cursor--; MoveCursorTo(prompt, cursor); continue; }
|
|
if (key.Key == ConsoleKey.RightArrow && cursor < buffer.Count) { cursor++; MoveCursorTo(prompt, cursor); continue; }
|
|
|
|
// ── Tab — autocomplete ────────────────────────────────────
|
|
if (key.Key == ConsoleKey.Tab)
|
|
{
|
|
var suggestions = GetSuggestions(new string(buffer.ToArray()));
|
|
if (suggestions.Length == 0) continue;
|
|
|
|
if (tab is null) tabIdx = 0;
|
|
else tabIdx = (tabIdx + 1) % suggestions.Length;
|
|
|
|
tab = suggestions[tabIdx];
|
|
|
|
// Replace word after last space with suggestion
|
|
var parts = new string(buffer.ToArray()).Split(' ');
|
|
parts[^1] = tab;
|
|
var completed = string.Join(' ', parts);
|
|
buffer = [.. completed];
|
|
cursor = buffer.Count;
|
|
RedrawLine(prompt, buffer, cursor);
|
|
continue;
|
|
}
|
|
|
|
tab = null;
|
|
|
|
// ── Regular character ─────────────────────────────────────
|
|
if (!char.IsControl(key.KeyChar))
|
|
{
|
|
buffer.Insert(cursor, key.KeyChar);
|
|
cursor++;
|
|
RedrawLine(prompt, buffer, cursor);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RedrawLine(string prompt, List<char> buffer, int cursor)
|
|
{
|
|
var col = Markup.Remove(prompt).Length;
|
|
Console.CursorLeft = 0;
|
|
Console.Write(new string(' ', Console.WindowWidth - 1));
|
|
Console.CursorLeft = 0;
|
|
AnsiConsole.Markup(prompt);
|
|
Console.Write(new string(buffer.ToArray()));
|
|
Console.CursorLeft = col + cursor;
|
|
}
|
|
|
|
private static void MoveCursorTo(string prompt, int cursor) =>
|
|
Console.CursorLeft = Markup.Remove(prompt).Length + cursor;
|
|
|
|
private string[] GetSuggestions(string input)
|
|
{
|
|
if (_ctx is null) return [];
|
|
|
|
var parts = input.TrimStart().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
var partial = parts.Length == 0 ? "" : parts[^1];
|
|
|
|
if (parts.Length <= 1)
|
|
return _registry.Names
|
|
.Where(n => n.StartsWith(partial, StringComparison.OrdinalIgnoreCase))
|
|
.Order().ToArray();
|
|
|
|
var slugs = _ctx.NotesInView().Select(n => n.Slug.Split('/').Last());
|
|
var folders = _ctx.FoldersInView().Select(f => f + "/");
|
|
|
|
return slugs.Concat(folders)
|
|
.Where(s => s.StartsWith(partial, StringComparison.OrdinalIgnoreCase))
|
|
.Distinct().Order().ToArray();
|
|
}
|
|
}
|