Editor/HotCodeEditor/CodeView/FindBar.cs
using Sandbox;
using Editor;
using System;

/// <summary>
/// The find / replace strip shown above a <see cref="CodeView"/> (Ctrl+F / Ctrl+H).
/// Enter = next, Shift+Enter = previous, Esc = close.
/// </summary>
public class FindBar : Widget
{
	const string OptionsCookie = "hotcode.find.";

	private readonly CodeView _view;
	private readonly FindLineEdit _query;
	private readonly FindLineEdit _replace;
	private readonly Widget _replaceRow;
	private readonly IconButton _expand;
	private readonly IconButton _matchCase;
	private readonly IconButton _wholeWord;
	private readonly IconButton _regex;
	private readonly Label _status;

	public FindBar( CodeView view ) : base( view )
	{
		_view = view;

		Layout = Layout.Column();
		Layout.Margin = 4;
		Layout.Spacing = 4;
		SetStyles( $"background-color: {Theme.WidgetBackground.Hex};" );

		// Row 1: [>] [find........] [Aa] [ab] [.*]  3 of 12  [↑] [↓] [×]
		var findRow = Layout.AddRow();
		findRow.Spacing = 4;

		_expand = findRow.Add( new IconButton( "chevron_right", () => ShowReplace = !ShowReplace ) { ToolTip = "Toggle Replace (Ctrl+H)" } );

		_query = findRow.Add( new FindLineEdit( this ) { PlaceholderText = "Find" }, 1 );
		_query.TextEdited += _ => _view.RunSearch( keepPosition: false );
		_query.KeyHandler = e => HandleKey( e, isReplace: false );

		_matchCase = findRow.Add( MakeToggle( "match_case", "Match Case", "matchcase" ) );
		_wholeWord = findRow.Add( MakeToggle( "match_word", "Match Whole Word", "wholeword" ) );
		_regex = findRow.Add( MakeToggle( "regular_expression", "Use Regular Expression", "regex" ) );

		_status = findRow.Add( new Label( "", this ) { MinimumWidth = 80 } );
		_status.Alignment = TextFlag.Center;

		findRow.Add( new IconButton( "arrow_upward", () => _view.FindNext( backwards: true ) ) { ToolTip = "Previous Match (Shift+Enter)" } );
		findRow.Add( new IconButton( "arrow_downward", () => _view.FindNext( backwards: false ) ) { ToolTip = "Next Match (Enter)" } );
		findRow.Add( new IconButton( "close", _view.CloseFind ) { ToolTip = "Close (Escape)" } );

		// Row 2: [replace.....] [Replace] [Replace All]
		_replaceRow = Layout.Add( new Widget( this ) );
		_replaceRow.Layout = Layout.Row();
		_replaceRow.Layout.Spacing = 4;
		_replaceRow.Layout.AddSpacingCell( 26 );

		_replace = _replaceRow.Layout.Add( new FindLineEdit( _replaceRow ) { PlaceholderText = "Replace" }, 1 );
		_replace.KeyHandler = e => HandleKey( e, isReplace: true );

		_replaceRow.Layout.Add( new Button( "Replace", "find_replace", _replaceRow ) { Clicked = _view.ReplaceCurrent, ToolTip = "Replace (Enter in the replace box)" } );
		_replaceRow.Layout.Add( new Button( "Replace All", "done_all", _replaceRow ) { Clicked = _view.ReplaceAll, ToolTip = "Replace All (Ctrl+Alt+Enter)" } );
		_replaceRow.Visible = false;
	}

	private IconButton MakeToggle( string icon, string tooltip, string cookie )
	{
		var button = new IconButton( icon ) { ToolTip = tooltip, IsToggle = true };
		button.IsActive = EditorCookie.Get( OptionsCookie + cookie, false );
		button.OnToggled = active =>
		{
			EditorCookie.Set( OptionsCookie + cookie, active );
			_view.RunSearch( keepPosition: false );
		};
		return button;
	}

	public SearchOptions Options => new( _query.Text, _matchCase.IsActive, _wholeWord.IsActive, _regex.IsActive );

	public string Query
	{
		get => _query.Text;
		set => _query.Text = value;
	}

	public string Replacement => _replace.Text;

	public bool ShowReplace
	{
		get => _replaceRow.Visible;
		set
		{
			_replaceRow.Visible = value;
			_expand.Icon = value ? "expand_more" : "chevron_right";
		}
	}

	public void FocusQuery()
	{
		_query.Focus( false );
		_query.SelectAll();
	}

	public void SetStatus( int count, int current, string error )
	{
		if ( error is not null )
		{
			_status.Text = "Invalid";
			_status.ToolTip = error;
			_status.SetStyles( $"color: {Theme.Red.Hex};" );
			return;
		}

		_status.ToolTip = null;
		_status.SetStyles( count == 0 && !string.IsNullOrEmpty( _query.Text ) ? $"color: {Theme.Red.Hex};" : "" );
		_status.Text = string.IsNullOrEmpty( _query.Text ) ? ""
			: count == 0 ? "No results"
			: count >= TextSearch.MaxMatches ? $"{current + 1} of {count}+"
			: $"{current + 1} of {count}";
	}

	private bool HandleKey( KeyEvent e, bool isReplace )
	{
		switch ( e.Key )
		{
			case KeyCode.Escape:
				_view.CloseFind();
				return true;

			case KeyCode.Return or KeyCode.Enter when isReplace && e.HasCtrl && e.HasAlt:
				_view.ReplaceAll();
				return true;

			case KeyCode.Return or KeyCode.Enter when isReplace:
				_view.ReplaceCurrent();
				return true;

			case KeyCode.Return or KeyCode.Enter:
			case KeyCode.F3:
				_view.FindNext( backwards: e.HasShift );
				return true;

			case KeyCode.H when e.HasCtrl:
				ShowReplace = true;
				_replace.Focus( false );
				return true;
		}

		return false;
	}
}

/// <summary>
/// A LineEdit that lets its owner handle keys like Enter and Escape first.
/// </summary>
public class FindLineEdit : LineEdit
{
	public Func<KeyEvent, bool> KeyHandler;

	public FindLineEdit( Widget parent ) : base( parent )
	{
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		if ( KeyHandler?.Invoke( e ) == true )
		{
			e.Accepted = true;
			return;
		}

		base.OnKeyPress( e );
	}
}