Editor/HotCodeEditor/Output/ConsoleInput.cs
using Sandbox;
using Editor;
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// The console command box under the Output log.
/// <list type="bullet">
/// <item>Enter runs the command</item>
/// <item>Up / Down walk through previous commands (kept between sessions)</item>
/// <item>Suggestions pop up as you type; Tab completes the first one</item>
/// </list>
/// </summary>
public class ConsoleInput : LineEdit
{
	const string HistoryCookieName = "hotcode.console.history";
	const int MaxHistory = 100;
	const int MaxSuggestions = 15;

	public Action<string> CommandEntered;

	private readonly List<string> _history;

	/// <summary>
	/// Position while walking history with Up/Down; equal to the count when not browsing.
	/// </summary>
	private int _historyIndex;

	/// <summary>
	/// What was typed before browsing history started, restored when Down goes past the newest entry.
	/// </summary>
	private string _draft = "";

	public ConsoleInput( Widget parent ) : base( parent )
	{
		PlaceholderText = "Enter a console command...";
		SetStyles( $"font-family: '{Theme.MonospaceFont}';" );

		_history = EditorCookie.Get<List<string>>( HistoryCookieName, null ) ?? new();
		_historyIndex = _history.Count;

		SetAutoComplete( BuildSuggestions );
	}

	private void BuildSuggestions( Menu menu, string text )
	{
		if ( string.IsNullOrWhiteSpace( text ) ) return;

		foreach ( var result in Suggest( text ) )
		{
			var command = result.Command;
			var label = string.IsNullOrEmpty( result.Description ) ? command : $"{command}  -  {result.Description}";
			menu.AddOption( label, null, () =>
			{
				Text = command + " ";
				Focus( false );
			} );
		}
	}

	private static ConCmdAttribute.AutoCompleteResult[] Suggest( string text )
	{
		try
		{
			return EditorUtility.AutoComplete( text.Trim(), MaxSuggestions ) ?? Array.Empty<ConCmdAttribute.AutoCompleteResult>();
		}
		catch ( Exception e )
		{
			Log.Warning( $"[HotCode] Console autocomplete failed: {e.Message}" );
			return Array.Empty<ConCmdAttribute.AutoCompleteResult>();
		}
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		switch ( e.Key )
		{
			case KeyCode.Return:
			case KeyCode.Enter:
				Submit();
				e.Accepted = true;
				return;

			case KeyCode.Up:
				BrowseHistory( -1 );
				e.Accepted = true;
				return;

			case KeyCode.Down:
				BrowseHistory( 1 );
				e.Accepted = true;
				return;

			case KeyCode.Tab when !string.IsNullOrWhiteSpace( Text ):
				var first = Suggest( Text ).FirstOrDefault();
				if ( !string.IsNullOrEmpty( first.Command ) ) Text = first.Command + " ";
				e.Accepted = true;
				return;

			case KeyCode.Escape:
				Text = "";
				_historyIndex = _history.Count;
				e.Accepted = true;
				return;
		}

		base.OnKeyPress( e );
	}

	private void Submit()
	{
		var command = Text.Trim();
		if ( command.Length == 0 ) return;

		// Most recent last, no repeats
		_history.Remove( command );
		_history.Add( command );
		if ( _history.Count > MaxHistory ) _history.RemoveRange( 0, _history.Count - MaxHistory );
		EditorCookie.Set( HistoryCookieName, _history );

		_historyIndex = _history.Count;
		_draft = "";
		Text = "";

		CommandEntered?.Invoke( command );
	}

	private void BrowseHistory( int direction )
	{
		if ( _history.Count == 0 ) return;

		if ( _historyIndex == _history.Count ) _draft = Text;

		_historyIndex = Math.Clamp( _historyIndex + direction, 0, _history.Count );
		Text = _historyIndex == _history.Count ? _draft : _history[_historyIndex];
		CursorPosition = Text.Length;
	}
}