فهرست منبع

Fixes #3966. TextField crashes app when pasting unicode surrogate pair (#3982)

* Add constructor Key(int) and operator for handled with non-Bmp.

* Fix TextField non-BMP issues

* Fix TextField PositionCursor.

* Reformat

* Add IsValidInput method to handle clipboard paste when pressing CTRL+V in WT

* Add handle IsValidInput in FakeDriver and unit tests

* Fixes #3984 - `Margin` w/out shadow should not force draw (#3985)

* shortcut tests

* Generic demos

* Optimize Margin to not defer draw if there's no shadow

* Fixes #4041. WSLClipboard doesn't handles well with surrogate pairs

* Avoid running Clipboard.Contents twice

* Fixes #4042. Microsoft.VisualStudio.TestPlatform.ObjectModel.TestPlatformException: Could not find testhost

* Moving tests to the parallelizable unit tests

* Remove unused folder

* Prevent warnings about not installed nuget packages

* Using Toplevel instead of Application.Top

* Cleanup code

---------

Co-authored-by: Tig <[email protected]>
BDisp 4 ماه پیش
والد
کامیت
5c03b96242

+ 1 - 1
Terminal.Gui/Clipboard/ClipboardBase.cs

@@ -22,7 +22,7 @@ public abstract class ClipboardBase : IClipboard
                 return string.Empty;
             }
 
-            return GetClipboardDataImpl ();
+            return result;
         }
         catch (NotSupportedException ex)
         {

+ 34 - 0
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -691,6 +691,40 @@ public abstract class ConsoleDriver : IConsoleDriver
     /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
     public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
 
+    internal char _highSurrogate = '\0';
+
+    internal bool IsValidInput (KeyCode keyCode, out KeyCode result)
+    {
+        result = keyCode;
+
+        if (char.IsHighSurrogate ((char)keyCode))
+        {
+            _highSurrogate = (char)keyCode;
+
+            return false;
+        }
+
+        if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode))
+        {
+            result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value;
+            _highSurrogate = '\0';
+
+            return true;
+        }
+
+        if (char.IsSurrogate ((char)keyCode))
+        {
+            return false;
+        }
+
+        if (_highSurrogate > 0)
+        {
+            _highSurrogate = '\0';
+        }
+
+        return true;
+    }
+
     #endregion
 
     private AnsiRequestScheduler? _scheduler;

+ 1 - 1
Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs

@@ -203,7 +203,7 @@ internal class WSLClipboard : ClipboardBase
         }
 
         (int exitCode, string output) =
-            ClipboardProcessRunner.Process (_powershellPath, "-noprofile -command \"Get-Clipboard\"");
+            ClipboardProcessRunner.Process (_powershellPath, "-noprofile -command \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Clipboard\"");
 
         if (exitCode == 0)
         {

+ 5 - 2
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -924,8 +924,11 @@ internal class CursesDriver : ConsoleDriver
                 k &= ~KeyCode.Space;
             }
 
-            OnKeyDown (new Key (k));
-            OnKeyUp (new Key (k));
+            if (IsValidInput (k, out k))
+            {
+                OnKeyDown (new (k));
+                OnKeyUp (new (k));
+            }
         }
     }
 

+ 6 - 2
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -352,8 +352,12 @@ public class FakeDriver : ConsoleDriver
         }
 
         KeyCode map = MapKey (consoleKeyInfo);
-        OnKeyDown (new Key (map));
-        OnKeyUp (new Key (map));
+
+        if (IsValidInput (map, out map))
+        {
+            OnKeyDown (new (map));
+            OnKeyUp (new (map));
+        }
 
         //OnKeyPressed (new KeyEventArgs (map));
     }

+ 5 - 2
Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs

@@ -321,8 +321,11 @@ internal class NetDriver : ConsoleDriver
                     break;
                 }
 
-                OnKeyDown (new (map));
-                OnKeyUp (new (map));
+                if (IsValidInput (map, out map))
+                {
+                    OnKeyDown (new (map));
+                    OnKeyUp (new (map));
+                }
 
                 break;
             case EventType.Mouse:

+ 6 - 3
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs

@@ -507,9 +507,12 @@ internal class WindowsDriver : ConsoleDriver
                     break;
                 }
 
-                // This follows convention in NetDriver
-                OnKeyDown (new Key (map));
-                OnKeyUp (new Key (map));
+                if (IsValidInput (map, out map))
+                {
+                    // This follows convention in NetDriver
+                    OnKeyDown (new (map));
+                    OnKeyUp (new (map));
+                }
 
                 break;
 

+ 51 - 1
Terminal.Gui/Input/Keyboard/Key.cs

@@ -138,6 +138,44 @@ public class Key : EventArgs, IEquatable<Key>
         KeyCode = key.KeyCode;
     }
 
+    /// <summary>
+    ///     Constructs a new Key from an integer describing the key.
+    ///     It parses the integer as Key by calling the constructor with a char or calls the constructor with a
+    ///     KeyCode.
+    /// </summary>
+    /// <remarks>
+    ///     Don't rely on <paramref name="value"/> passed from <see cref="KeyCode.A"/> to <see cref="KeyCode.Z"/> because
+    ///     would not return the expected keys from 'a' to 'z'.
+    /// </remarks>
+    /// <param name="value">The integer describing the key.</param>
+    /// <exception cref="ArgumentOutOfRangeException"></exception>
+    /// <exception cref="ArgumentException"></exception>
+    public Key (int value)
+    {
+        if (value < 0 || value > RuneExtensions.MaxUnicodeCodePoint)
+        {
+            throw new ArgumentOutOfRangeException (@$"Invalid key value: {value}", nameof (value));
+        }
+
+        if (char.IsSurrogate ((char)value))
+        {
+            throw new ArgumentException (@$"Surrogate key not allowed: {value}", nameof (value));
+        }
+
+        Key key;
+
+        if (((Rune)value).IsBmp)
+        {
+            key = new ((char)value);
+        }
+        else
+        {
+            key = new ((KeyCode)value);
+        }
+
+        KeyCode = key.KeyCode;
+    }
+
     /// <summary>
     ///     The key value as a Rune. This is the actual value of the key pressed, and is independent of the modifiers.
     ///     Useful for determining if a key represents is a printable character.
@@ -388,6 +426,11 @@ public class Key : EventArgs, IEquatable<Key>
     /// <param name="str"></param>
     public static implicit operator Key (string str) { return new (str); }
 
+    /// <summary>Cast <see langword="int"/> to a <see cref="Key"/>.</summary>
+    /// <remarks>See <see cref="Key(int)"/> for more information.</remarks>
+    /// <param name="value"></param>
+    public static implicit operator Key (int value) { return new (value); }
+
     /// <summary>Cast a <see cref="Key"/> to a <see langword="string"/>.</summary>
     /// <remarks>See <see cref="Key(string)"/> for more information.</remarks>
     /// <param name="key"></param>
@@ -550,7 +593,7 @@ public class Key : EventArgs, IEquatable<Key>
         // "Ctrl+" (trim)
         // "Ctrl++" (trim)
 
-        if (input.Length > 1 && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator)
+        if (input.Length > 1 && !char.IsHighSurrogate (input [^2]) && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator)
         {
             return input [..^1];
         }
@@ -640,6 +683,13 @@ public class Key : EventArgs, IEquatable<Key>
             return false;
         }
 
+        if (text.Length == 2 && char.IsHighSurrogate (text [^2]) && char.IsLowSurrogate (text [^1]))
+        {
+            // It's a surrogate pair and there is no modifiers
+            key = new (new Rune (text [^2], text [^1]).Value);
+            return true;
+        }
+
         // e.g. "Ctrl++"
         if ((Rune)text [^1] != separator && parts.Any (string.IsNullOrEmpty))
         {

+ 13 - 17
Terminal.Gui/Views/TextField.cs

@@ -729,21 +729,11 @@ public class TextField : View
     /// <param name="useOldCursorPos">Use the previous cursor position.</param>
     public void InsertText (string toAdd, bool useOldCursorPos = true)
     {
-        foreach (char ch in toAdd)
+        foreach (Rune rune in toAdd.EnumerateRunes ())
         {
-            Key key;
-
-            try
-            {
-                key = ch;
-            }
-            catch (Exception)
-            {
-                throw new ArgumentException (
-                                             $"Cannot insert character '{ch}' because it does not map to a Key"
-                                            );
-            }
-
+            // All rune can be mapped to a Key and no exception will throw here because
+            // EnumerateRunes will replace a surrogate char with the Rune.ReplacementChar
+            Key key = rune.Value;
             InsertText (key, useOldCursorPos);
         }
     }
@@ -1072,14 +1062,20 @@ public class TextField : View
     /// <summary>Paste the selected text from the clipboard.</summary>
     public virtual void Paste ()
     {
-        if (ReadOnly || string.IsNullOrEmpty (Clipboard.Contents))
+        if (ReadOnly)
+        {
+            return;
+        }
+
+        string cbTxt = Clipboard.Contents.Split ("\n") [0] ?? "";
+
+        if (string.IsNullOrEmpty (cbTxt))
         {
             return;
         }
 
         SetSelectedStartSelectedLength ();
         int selStart = _start == -1 ? CursorPosition : _start;
-        string cbTxt = Clipboard.Contents.Split ("\n") [0] ?? "";
 
         Text = StringExtensions.ToString (_text.GetRange (0, selStart))
                + cbTxt
@@ -1114,7 +1110,7 @@ public class TextField : View
             TextModel.SetCol (ref col, Viewport.Width - 1, cols);
         }
 
-        int pos = _cursorPosition - ScrollOffset + Math.Min (Viewport.X, 0);
+        int pos = col - ScrollOffset + Math.Min (Viewport.X, 0);
         Move (pos, 0);
 
         return new Point (pos, 0);

+ 0 - 14
TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj

@@ -1,14 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <Nullable>enable</Nullable>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
-    <PackageReference Include="xunit" />
-  </ItemGroup>
-
-</Project>

+ 0 - 25
TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs

@@ -1,25 +0,0 @@
-using Xunit;
-
-namespace TerminalGuiFluentTesting;
-
-public static class XunitContextExtensions
-{
-    public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition)
-    {
-        context.Then (
-                      () =>
-                      {
-                          Assert.True (condition);
-                      });
-        return context;
-    }
-    public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual)
-    {
-        context.Then (
-                      () =>
-                      {
-                          Assert.Equal (expected,actual);
-                      });
-        return context;
-    }
-}

+ 2 - 0
TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj

@@ -11,7 +11,9 @@
   <ItemGroup>
     <ProjectReference Include="..\TerminalGuiFluentTestingXunit.Generator\TerminalGuiFluentTestingXunit.Generator.csproj" OutputItemType="Analyzer" />
     <ProjectReference Include="..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="xunit" />
+    <PackageReference Include="xunit.runner.visualstudio" />
   </ItemGroup>
 
 </Project>

+ 111 - 0
Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs

@@ -283,4 +283,115 @@ public class ConsoleDriverTests
     //			Application.Run (win);
     //			Application.Shutdown ();
     //		}
+
+    [Theory]
+    [InlineData ('\ud83d', '\udcc4')] // This seems right sequence but Stack is LIFO
+    [InlineData ('\ud83d', '\ud83d')]
+    [InlineData ('\udcc4', '\udcc4')]
+    public void FakeDriver_IsValidInput_Wrong_Surrogate_Sequence (char c1, char c2)
+    {
+        var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver));
+        Application.Init (driver);
+
+        Stack<ConsoleKeyInfo> mKeys = new (
+                                           [
+                                               new ('a', ConsoleKey.A, false, false, false),
+                                               new (c1, ConsoleKey.None, false, false, false),
+                                               new (c2, ConsoleKey.None, false, false, false)
+                                           ]);
+
+        Console.MockKeyPresses = mKeys;
+
+        Toplevel top = new ();
+        var view = new View { CanFocus = true };
+        var rText = "";
+        var idx = 0;
+
+        view.KeyDown += (s, e) =>
+                        {
+                            Assert.Equal (new ('a'), e.AsRune);
+                            Assert.Equal ("a", e.AsRune.ToString ());
+                            rText += e.AsRune;
+                            e.Handled = true;
+                            idx++;
+                        };
+        top.Add (view);
+
+        Application.Iteration += (s, a) =>
+                                 {
+                                     if (mKeys.Count == 0)
+                                     {
+                                         Application.RequestStop ();
+                                     }
+                                 };
+
+        Application.Run (top);
+
+        Assert.Equal ("a", rText);
+        Assert.Equal (1, idx);
+        Assert.Equal (0, ((FakeDriver)driver)._highSurrogate);
+
+        top.Dispose ();
+
+        // Shutdown must be called to safely clean up Application if Init has been called
+        Application.Shutdown ();
+    }
+
+    [Fact]
+    public void FakeDriver_IsValidInput_Correct_Surrogate_Sequence ()
+    {
+        var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver));
+        Application.Init (driver);
+
+        Stack<ConsoleKeyInfo> mKeys = new (
+                                           [
+                                               new ('a', ConsoleKey.A, false, false, false),
+                                               new ('\udcc4', ConsoleKey.None, false, false, false),
+                                               new ('\ud83d', ConsoleKey.None, false, false, false)
+                                           ]);
+
+        Console.MockKeyPresses = mKeys;
+
+        Toplevel top = new ();
+        var view = new View { CanFocus = true };
+        var rText = "";
+        var idx = 0;
+
+        view.KeyDown += (s, e) =>
+                        {
+                            if (idx == 0)
+                            {
+                                Assert.Equal (new (0x1F4C4), e.AsRune);
+                                Assert.Equal ("📄", e.AsRune.ToString ());
+                            }
+                            else
+                            {
+                                Assert.Equal (new ('a'), e.AsRune);
+                                Assert.Equal ("a", e.AsRune.ToString ());
+                            }
+
+                            rText += e.AsRune;
+                            e.Handled = true;
+                            idx++;
+                        };
+        top.Add (view);
+
+        Application.Iteration += (s, a) =>
+                                 {
+                                     if (mKeys.Count == 0)
+                                     {
+                                         Application.RequestStop ();
+                                     }
+                                 };
+
+        Application.Run (top);
+
+        Assert.Equal ("📄a", rText);
+        Assert.Equal (2, idx);
+
+        top.Dispose ();
+
+        // Shutdown must be called to safely clean up Application if Init has been called
+        Application.Shutdown ();
+    }
 }

+ 0 - 520
Tests/UnitTests/Views/TextFieldTests.cs

@@ -78,27 +78,6 @@ public class TextFieldTests (ITestOutputHelper output)
         }
     }
 
-    [Fact]
-    public void Cancel_TextChanging_ThenBackspace ()
-    {
-        var tf = new TextField ();
-        tf.SetFocus ();
-        tf.NewKeyDownEvent (Key.A.WithShift);
-        Assert.Equal ("A", tf.Text);
-
-        // cancel the next keystroke
-        tf.TextChanging += (s, e) => e.Cancel = e.NewValue == "AB";
-        tf.NewKeyDownEvent (Key.B.WithShift);
-
-        // B was canceled so should just be A
-        Assert.Equal ("A", tf.Text);
-
-        // now delete the A
-        tf.NewKeyDownEvent (Key.Backspace);
-
-        Assert.Equal ("", tf.Text);
-    }
-
     [Fact]
     [TextFieldTestsAutoInitShutdown]
     public void CanFocus_False_Wont_Focus_With_Mouse ()
@@ -506,77 +485,6 @@ public class TextFieldTests (ITestOutputHelper output)
         top.Dispose ();
     }
 
-    [Fact]
-    public void HistoryText_IsDirty_ClearHistoryChanges ()
-    {
-        var text = "Testing";
-        var tf = new TextField { Text = text };
-        tf.BeginInit ();
-        tf.EndInit ();
-
-        Assert.Equal (text, tf.Text);
-        tf.ClearHistoryChanges ();
-        Assert.False (tf.IsDirty);
-
-        Assert.True (tf.NewKeyDownEvent (Key.A.WithShift));
-        Assert.Equal ($"{text}A", tf.Text);
-        Assert.True (tf.IsDirty);
-    }
-
-    [Fact]
-    public void Space_Does_Not_Raise_Selected ()
-    {
-        TextField tf = new ();
-
-        tf.Selecting += (sender, args) => Assert.Fail ("Selected should not be raied.");
-
-        Application.Top = new ();
-        Application.Top.Add (tf);
-        tf.SetFocus ();
-        Application.RaiseKeyDownEvent (Key.Space);
-
-        Application.Top.Dispose ();
-        Application.ResetState (true);
-    }
-
-    [Fact]
-    public void Enter_Does_Not_Raise_Selected ()
-    {
-        TextField tf = new ();
-
-        var selectingCount = 0;
-        tf.Selecting += (sender, args) => selectingCount++;
-
-        Application.Top = new ();
-        Application.Top.Add (tf);
-        tf.SetFocus ();
-        Application.RaiseKeyDownEvent (Key.Enter);
-
-        Assert.Equal (0, selectingCount);
-
-        Application.Top.Dispose ();
-        Application.ResetState (true);
-    }
-
-    [Fact]
-    public void Enter_Raises_Accepted ()
-    {
-        TextField tf = new ();
-
-        var acceptedCount = 0;
-        tf.Accepting += (sender, args) => acceptedCount++;
-
-        Application.Top = new ();
-        Application.Top.Add (tf);
-        tf.SetFocus ();
-        Application.RaiseKeyDownEvent (Key.Enter);
-
-        Assert.Equal (1, acceptedCount);
-
-        Application.Top.Dispose ();
-        Application.ResetState (true);
-    }
-
     [Fact]
     [AutoInitShutdown (useFakeClipboard: true)]
     public void KeyBindings_Command ()
@@ -811,47 +719,6 @@ public class TextFieldTests (ITestOutputHelper output)
         Assert.Equal ("", tf.Text);
     }
 
-    [Fact]
-    public void HotKey_Command_SetsFocus ()
-    {
-        var view = new TextField ();
-
-        view.CanFocus = true;
-        Assert.False (view.HasFocus);
-        view.InvokeCommand (Command.HotKey);
-        Assert.True (view.HasFocus);
-    }
-
-    [Fact]
-    public void HotKey_Command_Does_Not_Accept ()
-    {
-        var view = new TextField ();
-        var accepted = false;
-        view.Accepting += OnAccept;
-        view.InvokeCommand (Command.HotKey);
-
-        Assert.False (accepted);
-
-        return;
-
-        void OnAccept (object sender, CommandEventArgs e) { accepted = true; }
-    }
-
-    [Fact]
-    public void Accepted_Command_Fires_Accept ()
-    {
-        var view = new TextField ();
-
-        var accepted = false;
-        view.Accepting += Accept;
-        view.InvokeCommand (Command.Accept);
-        Assert.True (accepted);
-
-        return;
-
-        void Accept (object sender, CommandEventArgs e) { accepted = true; }
-    }
-
     [Theory]
     [InlineData (false, 1)]
     [InlineData (true, 0)]
@@ -904,87 +771,6 @@ public class TextFieldTests (ITestOutputHelper output)
         void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; }
     }
 
-    [Fact]
-    public void Accepted_No_Handler_Enables_Default_Button_Accept ()
-    {
-        var superView = new Window
-        {
-            Id = "superView"
-        };
-
-        var tf = new TextField
-        {
-            Id = "tf"
-        };
-
-        var button = new Button
-        {
-            Id = "button",
-            IsDefault = true
-        };
-
-        superView.Add (tf, button);
-
-        var buttonAccept = 0;
-        button.Accepting += ButtonAccept;
-
-        tf.SetFocus ();
-        Assert.True (tf.HasFocus);
-
-        superView.NewKeyDownEvent (Key.Enter);
-        Assert.Equal (1, buttonAccept);
-
-        button.SetFocus ();
-        superView.NewKeyDownEvent (Key.Enter);
-        Assert.Equal (2, buttonAccept);
-
-        return;
-
-        void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; }
-    }
-
-    [Fact]
-    public void Accepted_Cancel_Event_HandlesCommand ()
-    {
-        //var super = new View ();
-        var view = new TextField ();
-
-        //super.Add (view);
-
-        //var superAcceptedInvoked = false;
-
-        var tfAcceptedInvoked = false;
-        var handle = false;
-        view.Accepting += TextViewAccept;
-        Assert.False (view.InvokeCommand (Command.Accept));
-        Assert.True (tfAcceptedInvoked);
-
-        tfAcceptedInvoked = false;
-        handle = true;
-        view.Accepting += TextViewAccept;
-        Assert.True (view.InvokeCommand (Command.Accept));
-        Assert.True (tfAcceptedInvoked);
-
-        return;
-
-        void TextViewAccept (object sender, CommandEventArgs e)
-        {
-            tfAcceptedInvoked = true;
-            e.Cancel = handle;
-        }
-    }
-
-    [Fact]
-    public void OnEnter_Does_Not_Throw_If_Not_IsInitialized_SetCursorVisibility ()
-    {
-        var top = new Toplevel ();
-        var tf = new TextField { Width = 10 };
-        top.Add (tf);
-
-        Exception exception = Record.Exception (() => tf.SetFocus ());
-        Assert.Null (exception);
-    }
-
     [Fact]
     [TextFieldTestsAutoInitShutdown]
     public void Paste_Always_Clear_The_SelectedText ()
@@ -997,59 +783,6 @@ public class TextFieldTests (ITestOutputHelper output)
         Assert.Null (_textField.SelectedText);
     }
 
-    [Fact]
-    public void Backspace_From_End ()
-    {
-        var tf = new TextField { Text = "ABC" };
-        tf.SetFocus ();
-        Assert.Equal ("ABC", tf.Text);
-        tf.BeginInit ();
-        tf.EndInit ();
-
-        Assert.Equal (3, tf.CursorPosition);
-
-        // now delete the C
-        tf.NewKeyDownEvent (Key.Backspace);
-        Assert.Equal ("AB", tf.Text);
-        Assert.Equal (2, tf.CursorPosition);
-
-        // then delete the B
-        tf.NewKeyDownEvent (Key.Backspace);
-        Assert.Equal ("A", tf.Text);
-        Assert.Equal (1, tf.CursorPosition);
-
-        // then delete the A
-        tf.NewKeyDownEvent (Key.Backspace);
-        Assert.Equal ("", tf.Text);
-        Assert.Equal (0, tf.CursorPosition);
-    }
-
-    [Fact]
-    public void Backspace_From_Middle ()
-    {
-        var tf = new TextField { Text = "ABC" };
-        tf.SetFocus ();
-        tf.CursorPosition = 2;
-        Assert.Equal ("ABC", tf.Text);
-
-        // now delete the B
-        tf.NewKeyDownEvent (Key.Backspace);
-        Assert.Equal ("AC", tf.Text);
-
-        // then delete the A
-        tf.NewKeyDownEvent (Key.Backspace);
-        Assert.Equal ("C", tf.Text);
-
-        // then delete nothing
-        tf.NewKeyDownEvent (Key.Backspace);
-        Assert.Equal ("C", tf.Text);
-
-        // now delete the C
-        tf.CursorPosition = 1;
-        tf.NewKeyDownEvent (Key.Backspace);
-        Assert.Equal ("", tf.Text);
-    }
-
     [Fact]
     [AutoInitShutdown]
     public void ScrollOffset_Initialize ()
@@ -1142,36 +875,6 @@ public class TextFieldTests (ITestOutputHelper output)
         Assert.Null (_textField.SelectedText);
     }
 
-    [Fact]
-    public void KeyDown_Handled_Prevents_Input ()
-    {
-        var tf = new TextField ();
-        tf.KeyDown += HandleJKey;
-
-        tf.NewKeyDownEvent (Key.A);
-        Assert.Equal ("a", tf.Text);
-
-        // SuppressKey suppresses the 'j' key
-        tf.NewKeyDownEvent (Key.J);
-        Assert.Equal ("a", tf.Text);
-
-        tf.KeyDown -= HandleJKey;
-
-        // Now that the delegate has been removed we can type j again
-        tf.NewKeyDownEvent (Key.J);
-        Assert.Equal ("aj", tf.Text);
-
-        return;
-
-        void HandleJKey (object s, Key arg)
-        {
-            if (arg.AsRune == new Rune ('j'))
-            {
-                arg.Handled = true;
-            }
-        }
-    }
-
     [Fact]
     [AutoInitShutdown]
     public void MouseEvent_Handled_Prevents_RightClick ()
@@ -1222,33 +925,6 @@ public class TextFieldTests (ITestOutputHelper output)
         }
     }
 
-    [InlineData ("a")] // Lower than selection
-    [InlineData ("aaaaaaaaaaa")] // Greater than selection
-    [InlineData ("aaaa")] // Equal than selection
-    [Theory]
-    public void SetTextAndMoveCursorToEnd_WhenExistingSelection (string newText)
-    {
-        var tf = new TextField ();
-        tf.Text = "fish";
-        tf.CursorPosition = tf.Text.Length;
-
-        tf.NewKeyDownEvent (Key.CursorLeft);
-
-        tf.NewKeyDownEvent (Key.CursorLeft.WithShift);
-        tf.NewKeyDownEvent (Key.CursorLeft.WithShift);
-
-        Assert.Equal (1, tf.CursorPosition);
-        Assert.Equal (2, tf.SelectedLength);
-        Assert.Equal ("is", tf.SelectedText);
-
-        tf.Text = newText;
-        tf.CursorPosition = tf.Text.Length;
-
-        Assert.Equal (newText.Length, tf.CursorPosition);
-        Assert.Equal (0, tf.SelectedLength);
-        Assert.Null (tf.SelectedText);
-    }
-
     [Fact]
     [TextFieldTestsAutoInitShutdown]
     public void Text_Replaces_Tabs_With_Empty_String ()
@@ -1296,22 +972,6 @@ public class TextFieldTests (ITestOutputHelper output)
         Assert.Equal ("changing", _textField.Text);
     }
 
-    [Fact]
-    public void SpaceHandling ()
-    {
-        var tf = new TextField { Width = 10, Text = " " };
-
-        var ev = new MouseEventArgs { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked };
-
-        tf.NewMouseEvent (ev);
-        Assert.Equal (1, tf.SelectedLength);
-
-        ev = new () { Position = new (1, 0), Flags = MouseFlags.Button1DoubleClicked };
-
-        tf.NewMouseEvent (ev);
-        Assert.Equal (1, tf.SelectedLength);
-    }
-
     [Fact]
     [TextFieldTestsAutoInitShutdown]
     public void Used_Is_False ()
@@ -1631,77 +1291,6 @@ public class TextFieldTests (ITestOutputHelper output)
         }
     }
 
-    [Fact]
-    public void WordBackward_WordForward_Mixed ()
-    {
-        var tf = new TextField { Width = 30, Text = "Test with0. and!.?;-@+" };
-        tf.BeginInit ();
-        tf.EndInit ();
-
-        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
-        Assert.Equal (15, tf.CursorPosition);
-        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
-        Assert.Equal (12, tf.CursorPosition);
-        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
-        Assert.Equal (10, tf.CursorPosition);
-        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
-        Assert.Equal (5, tf.CursorPosition);
-        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
-        Assert.Equal (0, tf.CursorPosition);
-
-        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
-        Assert.Equal (5, tf.CursorPosition);
-        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
-        Assert.Equal (10, tf.CursorPosition);
-        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
-        Assert.Equal (12, tf.CursorPosition);
-        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
-        Assert.Equal (15, tf.CursorPosition);
-        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
-        Assert.Equal (22, tf.CursorPosition);
-    }
-
-    [Fact]
-    public void WordBackward_WordForward_SelectedText_With_Accent ()
-    {
-        var text = "Les Misérables movie.";
-        var tf = new TextField { Width = 30, Text = text };
-
-        Assert.Equal (21, text.Length);
-        Assert.Equal (21, tf.Text.GetRuneCount ());
-        Assert.Equal (21, tf.Text.GetColumns ());
-
-        List<Rune> runes = tf.Text.ToRuneList ();
-        Assert.Equal (21, runes.Count);
-        Assert.Equal (21, tf.Text.Length);
-
-        for (var i = 0; i < runes.Count; i++)
-        {
-            char cs = text [i];
-            var cus = (char)runes [i].Value;
-            Assert.Equal (cs, cus);
-        }
-
-        var idx = 15;
-        Assert.Equal ('m', text [idx]);
-        Assert.Equal ('m', (char)runes [idx].Value);
-        Assert.Equal ("m", runes [idx].ToString ());
-
-        Assert.True (
-                     tf.NewMouseEvent (
-                                       new () { Position = new (idx, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
-                                      )
-                    );
-        Assert.Equal ("movie.", tf.SelectedText);
-
-        Assert.True (
-                     tf.NewMouseEvent (
-                                       new () { Position = new (idx + 1, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
-                                      )
-                    );
-        Assert.Equal ("movie.", tf.SelectedText);
-    }
-
     [Fact]
     [TextFieldTestsAutoInitShutdown]
     public void WordForward_With_No_Selection ()
@@ -2056,115 +1645,6 @@ Les Miśerables",
         }
     }
 
-    [Fact]
-    public void Autocomplete_Popup_Added_To_SuperView_On_Init ()
-    {
-        View superView = new ()
-        {
-            CanFocus = true
-        };
-
-        TextField t = new ();
-
-        superView.Add (t);
-        Assert.Single (superView.SubViews);
-
-        superView.BeginInit ();
-        superView.EndInit ();
-
-        Assert.Equal (2, superView.SubViews.Count);
-    }
-
-    [Fact]
-    public void Autocomplete__Added_To_SuperView_On_Add ()
-    {
-        View superView = new ()
-        {
-            CanFocus = true,
-            Id = "superView"
-        };
-
-        superView.BeginInit ();
-        superView.EndInit ();
-        Assert.Empty (superView.SubViews);
-
-        TextField t = new ()
-        {
-            Id = "t"
-        };
-
-        superView.Add (t);
-
-        Assert.Equal (2, superView.SubViews.Count);
-    }
-
-    [Fact]
-    public void Right_CursorAtEnd_WithSelection_ShouldClearSelection ()
-    {
-        var tf = new TextField
-        {
-            Text = "Hello",
-        };
-        tf.SetFocus ();
-        tf.SelectAll ();
-        tf.CursorPosition = 5;
-
-        // When there is selected text and the cursor is at the end of the text field
-        Assert.Equal ("Hello", tf.SelectedText);
-
-        // Pressing right should not move focus, instead it should clear selection
-        Assert.True (tf.NewKeyDownEvent (Key.CursorRight));
-        Assert.Null (tf.SelectedText);
-
-        // Now that the selection is cleared another right keypress should move focus
-        Assert.False (tf.NewKeyDownEvent (Key.CursorRight));
-    }
-    [Fact]
-    public void Left_CursorAtStart_WithSelection_ShouldClearSelection ()
-    {
-        var tf = new TextField
-        {
-            Text = "Hello",
-        };
-        tf.SetFocus ();
-
-        tf.CursorPosition = 2;
-        Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
-        Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
-
-        // When there is selected text and the cursor is at the start of the text field
-        Assert.Equal ("He", tf.SelectedText);
-
-        // Pressing left should not move focus, instead it should clear selection
-        Assert.True (tf.NewKeyDownEvent (Key.CursorLeft));
-        Assert.Null (tf.SelectedText);
-
-        // When clearing selected text with left the cursor should be at the start of the selection
-        Assert.Equal (0, tf.CursorPosition);
-
-        // Now that the selection is cleared another left keypress should move focus
-        Assert.False (tf.NewKeyDownEvent (Key.CursorLeft));
-    }
-    [Fact]
-    public void Autocomplete_Visible_False_By_Default ()
-    {
-        View superView = new ()
-        {
-            CanFocus = true
-        };
-
-        TextField t = new ();
-
-        superView.Add (t);
-        superView.BeginInit ();
-        superView.EndInit ();
-
-        Assert.Equal (2, superView.SubViews.Count);
-
-        Assert.True (t.Visible);
-        Assert.False (t.Autocomplete.Visible);
-    }
-
     [Fact]
     [AutoInitShutdown]
     public void Draw_Esc_Rune ()

+ 42 - 11
Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs

@@ -52,7 +52,8 @@ public class KeyTests
             { "Ctrl-A", Key.A.WithCtrl },
             { "Alt-A", Key.A.WithAlt },
             { "A-Ctrl", Key.A.WithCtrl },
-            { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt }
+            { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt },
+            { "📄", (KeyCode)0x1F4C4 }
         };
 
     [Theory]
@@ -120,10 +121,13 @@ public class KeyTests
     [InlineData ('\'', (KeyCode)'\'')]
     [InlineData ('\xFFFF', (KeyCode)0xFFFF)]
     [InlineData ('\x0', (KeyCode)0x0)]
-    public void Cast_Char_To_Key (char ch, KeyCode expectedKeyCode)
+    public void Cast_Char_Int_To_Key (char ch, KeyCode expectedKeyCode)
     {
         var key = (Key)ch;
         Assert.Equal (expectedKeyCode, key.KeyCode);
+
+        key = (int)ch;
+        Assert.Equal (expectedKeyCode, key.KeyCode);
     }
 
     [Fact]
@@ -140,23 +144,25 @@ public class KeyTests
     [InlineData (KeyCode.A | KeyCode.ShiftMask, KeyCode.A | KeyCode.ShiftMask)]
     [InlineData (KeyCode.Z, KeyCode.Z)]
     [InlineData (KeyCode.Space, KeyCode.Space)]
-    public void Cast_KeyCode_To_Key (KeyCode cdk, KeyCode expected)
+    public void Cast_KeyCode_Int_To_Key (KeyCode cdk, KeyCode expected)
     {
-        // explicit
+        // KeyCode
         var key = (Key)cdk;
         Assert.Equal (((Key)expected).ToString (), key.ToString ());
 
-        // implicit
-        key = cdk;
+        // Int
+        key = key.AsRune.Value;
         Assert.Equal (((Key)expected).ToString (), key.ToString ());
     }
 
     // string cast operators
-    [Fact]
-    public void Cast_String_To_Key ()
+    [Theory]
+    [InlineData ("Ctrl+Q", KeyCode.Q | KeyCode.CtrlMask)]
+    [InlineData ("📄", (KeyCode)0x1F4C4)]
+    public void Cast_String_To_Key (string str, KeyCode expectedKeyCode)
     {
-        var key = (Key)"Ctrl+Q";
-        Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, key.KeyCode);
+        var key = (Key)str;
+        Assert.Equal (expectedKeyCode, key.KeyCode);
     }
 
     [Theory]
@@ -190,12 +196,37 @@ public class KeyTests
     [InlineData ('\'', (KeyCode)'\'')]
     [InlineData ('\xFFFF', (KeyCode)0xFFFF)]
     [InlineData ('\x0', (KeyCode)0x0)]
-    public void Constructor_Char (char ch, KeyCode expectedKeyCode)
+    public void Constructor_Char_Int (char ch, KeyCode expectedKeyCode)
     {
         var key = new Key (ch);
         Assert.Equal (expectedKeyCode, key.KeyCode);
+
+        key = new ((int)ch);
+        Assert.Equal (expectedKeyCode, key.KeyCode);
+    }
+
+    [Theory]
+    [InlineData (0x1F4C4, (KeyCode)0x1F4C4, "📄")]
+    [InlineData (0x1F64B, (KeyCode)0x1F64B, "🙋")]
+    [InlineData (0x1F46A, (KeyCode)0x1F46A, "👪")]
+    public void Constructor_Int_Non_Bmp (int value, KeyCode expectedKeyCode, string expectedString)
+    {
+        var key = new Key (value);
+        Assert.Equal (expectedKeyCode, key.KeyCode);
+        Assert.Equal (expectedString, key.AsRune.ToString ());
+        Assert.Equal (expectedString, key.ToString ());
     }
 
+    [Theory]
+    [InlineData (-1)]
+    [InlineData (0x11FFFF)]
+    public void Constructor_Int_Invalid_Throws (int keyInt) { Assert.Throws<ArgumentOutOfRangeException> (() => new Key (keyInt)); }
+
+    [Theory]
+    [InlineData ('\ud83d')]
+    [InlineData ('\udcc4')]
+    public void Constructor_Int_Surrogate_Throws (int keyInt) { Assert.Throws<ArgumentException> (() => new Key (keyInt)); }
+
     [Fact]
     public void Constructor_Default_ShouldSetKeyToNull ()
     {

+ 554 - 0
Tests/UnitTestsParallelizable/Views/TextFieldTests.cs

@@ -0,0 +1,554 @@
+using System.Text;
+
+namespace Terminal.Gui.ViewsTests;
+
+public class TextFieldTests
+{
+    [Fact]
+    public void Cancel_TextChanging_ThenBackspace ()
+    {
+        var tf = new TextField ();
+        tf.SetFocus ();
+        tf.NewKeyDownEvent (Key.A.WithShift);
+        Assert.Equal ("A", tf.Text);
+
+        // cancel the next keystroke
+        tf.TextChanging += (s, e) => e.Cancel = e.NewValue == "AB";
+        tf.NewKeyDownEvent (Key.B.WithShift);
+
+        // B was canceled so should just be A
+        Assert.Equal ("A", tf.Text);
+
+        // now delete the A
+        tf.NewKeyDownEvent (Key.Backspace);
+
+        Assert.Equal ("", tf.Text);
+    }
+
+    [Fact]
+    public void HistoryText_IsDirty_ClearHistoryChanges ()
+    {
+        var text = "Testing";
+        var tf = new TextField { Text = text };
+        tf.BeginInit ();
+        tf.EndInit ();
+
+        Assert.Equal (text, tf.Text);
+        tf.ClearHistoryChanges ();
+        Assert.False (tf.IsDirty);
+
+        Assert.True (tf.NewKeyDownEvent (Key.A.WithShift));
+        Assert.Equal ($"{text}A", tf.Text);
+        Assert.True (tf.IsDirty);
+    }
+
+    [Fact]
+    public void Space_Does_Not_Raise_Selected ()
+    {
+        TextField tf = new ();
+
+        tf.Selecting += (sender, args) => Assert.Fail ("Selected should not be raied.");
+
+        Toplevel top = new ();
+        top.Add (tf);
+        tf.SetFocus ();
+        top.NewKeyDownEvent (Key.Space);
+
+        top.Dispose ();
+    }
+
+    [Fact]
+    public void Enter_Does_Not_Raise_Selected ()
+    {
+        TextField tf = new ();
+
+        var selectingCount = 0;
+        tf.Selecting += (sender, args) => selectingCount++;
+
+        Toplevel top = new ();
+        top.Add (tf);
+        tf.SetFocus ();
+        top.NewKeyDownEvent (Key.Enter);
+
+        Assert.Equal (0, selectingCount);
+
+        top.Dispose ();
+    }
+
+    [Fact]
+    public void Enter_Raises_Accepted ()
+    {
+        TextField tf = new ();
+
+        var acceptedCount = 0;
+        tf.Accepting += (sender, args) => acceptedCount++;
+
+        Toplevel top = new ();
+        top.Add (tf);
+        tf.SetFocus ();
+        top.NewKeyDownEvent (Key.Enter);
+
+        Assert.Equal (1, acceptedCount);
+
+        top.Dispose ();
+    }
+
+    [Fact]
+    public void HotKey_Command_SetsFocus ()
+    {
+        var view = new TextField ();
+
+        view.CanFocus = true;
+        Assert.False (view.HasFocus);
+        view.InvokeCommand (Command.HotKey);
+        Assert.True (view.HasFocus);
+    }
+
+    [Fact]
+    public void HotKey_Command_Does_Not_Accept ()
+    {
+        var view = new TextField ();
+        var accepted = false;
+        view.Accepting += OnAccept;
+        view.InvokeCommand (Command.HotKey);
+
+        Assert.False (accepted);
+
+        return;
+
+        void OnAccept (object sender, CommandEventArgs e) { accepted = true; }
+    }
+
+    [Fact]
+    public void Accepted_Command_Fires_Accept ()
+    {
+        var view = new TextField ();
+
+        var accepted = false;
+        view.Accepting += Accept;
+        view.InvokeCommand (Command.Accept);
+        Assert.True (accepted);
+
+        return;
+
+        void Accept (object sender, CommandEventArgs e) { accepted = true; }
+    }
+
+    [Fact]
+    public void Accepted_No_Handler_Enables_Default_Button_Accept ()
+    {
+        var superView = new Window
+        {
+            Id = "superView"
+        };
+
+        var tf = new TextField
+        {
+            Id = "tf"
+        };
+
+        var button = new Button
+        {
+            Id = "button",
+            IsDefault = true
+        };
+
+        superView.Add (tf, button);
+
+        var buttonAccept = 0;
+        button.Accepting += ButtonAccept;
+
+        tf.SetFocus ();
+        Assert.True (tf.HasFocus);
+
+        superView.NewKeyDownEvent (Key.Enter);
+        Assert.Equal (1, buttonAccept);
+
+        button.SetFocus ();
+        superView.NewKeyDownEvent (Key.Enter);
+        Assert.Equal (2, buttonAccept);
+
+        return;
+
+        void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; }
+    }
+
+    [Fact]
+    public void Accepted_Cancel_Event_HandlesCommand ()
+    {
+        //var super = new View ();
+        var view = new TextField ();
+
+        //super.Add (view);
+
+        //var superAcceptedInvoked = false;
+
+        var tfAcceptedInvoked = false;
+        var handle = false;
+        view.Accepting += TextViewAccept;
+        Assert.False (view.InvokeCommand (Command.Accept));
+        Assert.True (tfAcceptedInvoked);
+
+        tfAcceptedInvoked = false;
+        handle = true;
+        view.Accepting += TextViewAccept;
+        Assert.True (view.InvokeCommand (Command.Accept));
+        Assert.True (tfAcceptedInvoked);
+
+        return;
+
+        void TextViewAccept (object sender, CommandEventArgs e)
+        {
+            tfAcceptedInvoked = true;
+            e.Cancel = handle;
+        }
+    }
+
+    [Fact]
+    public void OnEnter_Does_Not_Throw_If_Not_IsInitialized_SetCursorVisibility ()
+    {
+        var top = new Toplevel ();
+        var tf = new TextField { Width = 10 };
+        top.Add (tf);
+
+        Exception exception = Record.Exception (() => tf.SetFocus ());
+        Assert.Null (exception);
+    }
+
+    [Fact]
+    public void Backspace_From_End ()
+    {
+        var tf = new TextField { Text = "ABC" };
+        tf.SetFocus ();
+        Assert.Equal ("ABC", tf.Text);
+        tf.BeginInit ();
+        tf.EndInit ();
+
+        Assert.Equal (3, tf.CursorPosition);
+
+        // now delete the C
+        tf.NewKeyDownEvent (Key.Backspace);
+        Assert.Equal ("AB", tf.Text);
+        Assert.Equal (2, tf.CursorPosition);
+
+        // then delete the B
+        tf.NewKeyDownEvent (Key.Backspace);
+        Assert.Equal ("A", tf.Text);
+        Assert.Equal (1, tf.CursorPosition);
+
+        // then delete the A
+        tf.NewKeyDownEvent (Key.Backspace);
+        Assert.Equal ("", tf.Text);
+        Assert.Equal (0, tf.CursorPosition);
+    }
+
+    [Fact]
+    public void Backspace_From_Middle ()
+    {
+        var tf = new TextField { Text = "ABC" };
+        tf.SetFocus ();
+        tf.CursorPosition = 2;
+        Assert.Equal ("ABC", tf.Text);
+
+        // now delete the B
+        tf.NewKeyDownEvent (Key.Backspace);
+        Assert.Equal ("AC", tf.Text);
+
+        // then delete the A
+        tf.NewKeyDownEvent (Key.Backspace);
+        Assert.Equal ("C", tf.Text);
+
+        // then delete nothing
+        tf.NewKeyDownEvent (Key.Backspace);
+        Assert.Equal ("C", tf.Text);
+
+        // now delete the C
+        tf.CursorPosition = 1;
+        tf.NewKeyDownEvent (Key.Backspace);
+        Assert.Equal ("", tf.Text);
+    }
+
+    [Fact]
+    public void KeyDown_Handled_Prevents_Input ()
+    {
+        var tf = new TextField ();
+        tf.KeyDown += HandleJKey;
+
+        tf.NewKeyDownEvent (Key.A);
+        Assert.Equal ("a", tf.Text);
+
+        // SuppressKey suppresses the 'j' key
+        tf.NewKeyDownEvent (Key.J);
+        Assert.Equal ("a", tf.Text);
+
+        tf.KeyDown -= HandleJKey;
+
+        // Now that the delegate has been removed we can type j again
+        tf.NewKeyDownEvent (Key.J);
+        Assert.Equal ("aj", tf.Text);
+
+        return;
+
+        void HandleJKey (object s, Key arg)
+        {
+            if (arg.AsRune == new Rune ('j'))
+            {
+                arg.Handled = true;
+            }
+        }
+    }
+
+    [InlineData ("a")] // Lower than selection
+    [InlineData ("aaaaaaaaaaa")] // Greater than selection
+    [InlineData ("aaaa")] // Equal than selection
+    [Theory]
+    public void SetTextAndMoveCursorToEnd_WhenExistingSelection (string newText)
+    {
+        var tf = new TextField ();
+        tf.Text = "fish";
+        tf.CursorPosition = tf.Text.Length;
+
+        tf.NewKeyDownEvent (Key.CursorLeft);
+
+        tf.NewKeyDownEvent (Key.CursorLeft.WithShift);
+        tf.NewKeyDownEvent (Key.CursorLeft.WithShift);
+
+        Assert.Equal (1, tf.CursorPosition);
+        Assert.Equal (2, tf.SelectedLength);
+        Assert.Equal ("is", tf.SelectedText);
+
+        tf.Text = newText;
+        tf.CursorPosition = tf.Text.Length;
+
+        Assert.Equal (newText.Length, tf.CursorPosition);
+        Assert.Equal (0, tf.SelectedLength);
+        Assert.Null (tf.SelectedText);
+    }
+
+    [Fact]
+    public void SpaceHandling ()
+    {
+        var tf = new TextField { Width = 10, Text = " " };
+
+        var ev = new MouseEventArgs { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked };
+
+        tf.NewMouseEvent (ev);
+        Assert.Equal (1, tf.SelectedLength);
+
+        ev = new () { Position = new (1, 0), Flags = MouseFlags.Button1DoubleClicked };
+
+        tf.NewMouseEvent (ev);
+        Assert.Equal (1, tf.SelectedLength);
+    }
+
+    [Fact]
+    public void WordBackward_WordForward_Mixed ()
+    {
+        var tf = new TextField { Width = 30, Text = "Test with0. and!.?;-@+" };
+        tf.BeginInit ();
+        tf.EndInit ();
+
+        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+        Assert.Equal (15, tf.CursorPosition);
+        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+        Assert.Equal (12, tf.CursorPosition);
+        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+        Assert.Equal (10, tf.CursorPosition);
+        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+        Assert.Equal (5, tf.CursorPosition);
+        tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+        Assert.Equal (0, tf.CursorPosition);
+
+        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+        Assert.Equal (5, tf.CursorPosition);
+        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+        Assert.Equal (10, tf.CursorPosition);
+        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+        Assert.Equal (12, tf.CursorPosition);
+        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+        Assert.Equal (15, tf.CursorPosition);
+        tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+        Assert.Equal (22, tf.CursorPosition);
+    }
+
+    [Fact]
+    public void WordBackward_WordForward_SelectedText_With_Accent ()
+    {
+        var text = "Les Misérables movie.";
+        var tf = new TextField { Width = 30, Text = text };
+
+        Assert.Equal (21, text.Length);
+        Assert.Equal (21, tf.Text.GetRuneCount ());
+        Assert.Equal (21, tf.Text.GetColumns ());
+
+        List<Rune> runes = tf.Text.ToRuneList ();
+        Assert.Equal (21, runes.Count);
+        Assert.Equal (21, tf.Text.Length);
+
+        for (var i = 0; i < runes.Count; i++)
+        {
+            char cs = text [i];
+            var cus = (char)runes [i].Value;
+            Assert.Equal (cs, cus);
+        }
+
+        var idx = 15;
+        Assert.Equal ('m', text [idx]);
+        Assert.Equal ('m', (char)runes [idx].Value);
+        Assert.Equal ("m", runes [idx].ToString ());
+
+        Assert.True (
+                     tf.NewMouseEvent (
+                                       new () { Position = new (idx, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
+                                      )
+                    );
+        Assert.Equal ("movie.", tf.SelectedText);
+
+        Assert.True (
+                     tf.NewMouseEvent (
+                                       new () { Position = new (idx + 1, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
+                                      )
+                    );
+        Assert.Equal ("movie.", tf.SelectedText);
+    }
+
+    [Fact]
+    public void Autocomplete_Popup_Added_To_SuperView_On_Init ()
+    {
+        View superView = new ()
+        {
+            CanFocus = true
+        };
+
+        TextField t = new ();
+
+        superView.Add (t);
+        Assert.Single (superView.SubViews);
+
+        superView.BeginInit ();
+        superView.EndInit ();
+
+        Assert.Equal (2, superView.SubViews.Count);
+    }
+
+    [Fact]
+    public void Autocomplete__Added_To_SuperView_On_Add ()
+    {
+        View superView = new ()
+        {
+            CanFocus = true,
+            Id = "superView"
+        };
+
+        superView.BeginInit ();
+        superView.EndInit ();
+        Assert.Empty (superView.SubViews);
+
+        TextField t = new ()
+        {
+            Id = "t"
+        };
+
+        superView.Add (t);
+
+        Assert.Equal (2, superView.SubViews.Count);
+    }
+
+    [Fact]
+    public void Right_CursorAtEnd_WithSelection_ShouldClearSelection ()
+    {
+        var tf = new TextField
+        {
+            Text = "Hello"
+        };
+        tf.SetFocus ();
+        tf.SelectAll ();
+        tf.CursorPosition = 5;
+
+        // When there is selected text and the cursor is at the end of the text field
+        Assert.Equal ("Hello", tf.SelectedText);
+
+        // Pressing right should not move focus, instead it should clear selection
+        Assert.True (tf.NewKeyDownEvent (Key.CursorRight));
+        Assert.Null (tf.SelectedText);
+
+        // Now that the selection is cleared another right keypress should move focus
+        Assert.False (tf.NewKeyDownEvent (Key.CursorRight));
+    }
+
+    [Fact]
+    public void Left_CursorAtStart_WithSelection_ShouldClearSelection ()
+    {
+        var tf = new TextField
+        {
+            Text = "Hello"
+        };
+        tf.SetFocus ();
+
+        tf.CursorPosition = 2;
+        Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
+        Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
+
+        // When there is selected text and the cursor is at the start of the text field
+        Assert.Equal ("He", tf.SelectedText);
+
+        // Pressing left should not move focus, instead it should clear selection
+        Assert.True (tf.NewKeyDownEvent (Key.CursorLeft));
+        Assert.Null (tf.SelectedText);
+
+        // When clearing selected text with left the cursor should be at the start of the selection
+        Assert.Equal (0, tf.CursorPosition);
+
+        // Now that the selection is cleared another left keypress should move focus
+        Assert.False (tf.NewKeyDownEvent (Key.CursorLeft));
+    }
+
+    [Fact]
+    public void Autocomplete_Visible_False_By_Default ()
+    {
+        View superView = new ()
+        {
+            CanFocus = true
+        };
+
+        TextField t = new ();
+
+        superView.Add (t);
+        superView.BeginInit ();
+        superView.EndInit ();
+
+        Assert.Equal (2, superView.SubViews.Count);
+
+        Assert.True (t.Visible);
+        Assert.False (t.Autocomplete.Visible);
+    }
+
+    [Fact]
+    public void InsertText_Bmp_SurrogatePair_Non_Bmp_Invalid_SurrogatePair ()
+    {
+        var tf = new TextField ();
+
+        //📄 == \ud83d\udcc4 == \U0001F4C4
+        // � == Rune.ReplacementChar
+        tf.InsertText ("aA,;\ud83d\udcc4\U0001F4C4\udcc4\ud83d");
+        Assert.Equal ("aA,;📄📄��", tf.Text);
+    }
+
+    [Fact]
+    public void PositionCursor_Respect_GetColumns ()
+    {
+        var tf = new TextField { Width = 5 };
+        tf.BeginInit ();
+        tf.EndInit ();
+
+        tf.NewKeyDownEvent (new ("📄"));
+        Assert.Equal (1, tf.CursorPosition);
+        Assert.Equal (new (2, 0), tf.PositionCursor ());
+        Assert.Equal ("📄", tf.Text);
+
+        tf.NewKeyDownEvent (new (KeyCode.A));
+        Assert.Equal (2, tf.CursorPosition);
+        Assert.Equal (new (3, 0), tf.PositionCursor ());
+        Assert.Equal ("📄a", tf.Text);
+    }
+}