Browse Source

Added unicode edit support

Tig 9 months ago
parent
commit
11f2f301f5

+ 3 - 0
Terminal.Gui/Drawing/Glyphs.cs

@@ -97,6 +97,9 @@ public class GlyphDefinitions
     /// <summary>Dot. Default is (U+2219) - ∙.</summary>
     public Rune Dot { get; set; } = (Rune)'∙';
 
+    /// <summary>Dotted Square - ⬚ U+02b1a┝</summary>
+    public Rune DottedSquare { get; set; } = (Rune)'⬚';
+
     /// <summary>Black Circle . Default is (U+025cf) - ●.</summary>
     public Rune BlackCircle { get; set; } = (Rune)'●'; // Black Circle - ● U+025cf
 

+ 5 - 0
Terminal.Gui/Input/Command.cs

@@ -166,6 +166,11 @@ public enum Command
     /// </summary>
     EnableOverwrite,
 
+    /// <summary>
+    ///     Inserts a character.
+    /// </summary>
+    Insert,
+
     /// <summary>Disables overwrite mode (<see cref="EnableOverwrite"/>)</summary>
     DisableOverwrite,
 

+ 154 - 81
Terminal.Gui/Views/HexView.cs

@@ -1,21 +1,22 @@
 #nullable enable
 
-using System.Diagnostics;
-
 //
 // HexView.cs: A hexadecimal viewer
 //
 // TODO: Support searching and highlighting of the search result
-// TODO: Support growing/shrinking the stream (e.g. del/backspace should work).
+// TODO: Support shrinking the stream (e.g. del/backspace should work).
 // 
 
+using System.Buffers;
+
 namespace Terminal.Gui;
 
-/// <summary>An hex viewer and editor <see cref="View"/> over a <see cref="Stream"/></summary>
+/// <summary>Hex viewer and editor <see cref="View"/> over a <see cref="Stream"/></summary>
 /// <remarks>
 ///     <para>
 ///         <see cref="HexView"/> provides a hex editor on top of a seekable <see cref="Stream"/> with the left side
-///         showing an hex dump of the values in the <see cref="Stream"/> and the right side showing the contents (filtered
+///         showing the hex values of the bytes in the <see cref="Stream"/> and the right side showing the contents
+///         (filtered
 ///         to non-control sequence ASCII characters).
 ///     </para>
 ///     <para>Users can switch from one side to the other by using the tab key.</para>
@@ -26,6 +27,10 @@ namespace Terminal.Gui;
 ///         changes were made and the new values. A convenience method, <see cref="ApplyEdits"/> will apply the edits to
 ///         the <see cref="Stream"/>.
 ///     </para>
+///     <para>
+///         Control the byte at the caret for editing by setting the <see cref="Address"/> property to an offset in the
+///         stream.
+///     </para>
 ///     <para>Control the first byte shown by setting the <see cref="DisplayStart"/> property to an offset in the stream.</para>
 /// </remarks>
 public class HexView : View, IDesignable
@@ -37,7 +42,8 @@ public class HexView : View, IDesignable
     private bool _firstNibble;
     private bool _leftSideHasFocus;
     private static readonly Rune _spaceCharRune = new (' ');
-    private static readonly Rune _periodCharRune = new ('.');
+    private static readonly Rune _periodCharRune = Glyphs.DottedSquare;
+    private static readonly Rune _columnSeparatorRune = Glyphs.VLineDa4;
 
     /// <summary>Initializes a <see cref="HexView"/> class.</summary>
     /// <param name="source">
@@ -55,26 +61,27 @@ public class HexView : View, IDesignable
 
         // PERF: Closure capture of 'this' creates a lot of overhead.
         // BUG: Closure capture of 'this' may have unexpected results depending on how this is called.
-        // The above two comments apply to all of the lambdas passed to all calls to AddCommand below.
-        // Things this view knows how to do
+        // The above two comments apply to all the lambdas passed to all calls to AddCommand below.
         AddCommand (Command.Left, () => MoveLeft ());
         AddCommand (Command.Right, () => MoveRight ());
         AddCommand (Command.Down, () => MoveDown (BytesPerLine));
         AddCommand (Command.Up, () => MoveUp (BytesPerLine));
         AddCommand (Command.Tab, () => Navigate (NavigationDirection.Forward));
         AddCommand (Command.BackTab, () => Navigate (NavigationDirection.Backward));
-        AddCommand (Command.PageUp, () => MoveUp (BytesPerLine * Frame.Height));
-        AddCommand (Command.PageDown, () => MoveDown (BytesPerLine * Frame.Height));
+        AddCommand (Command.PageUp, () => MoveUp (BytesPerLine * Viewport.Height));
+        AddCommand (Command.PageDown, () => MoveDown (BytesPerLine * Viewport.Height));
         AddCommand (Command.Start, () => MoveHome ());
         AddCommand (Command.End, () => MoveEnd ());
         AddCommand (Command.LeftStart, () => MoveLeftStart ());
         AddCommand (Command.RightEnd, () => MoveEndOfLine ());
         AddCommand (Command.StartOfPage, () => MoveUp (BytesPerLine * ((int)(Address - _displayStart) / BytesPerLine)));
-
         AddCommand (
                     Command.EndOfPage,
-                    () => MoveDown (BytesPerLine * (Frame.Height - 1 - (int)(Address - _displayStart) / BytesPerLine))
+                    () => MoveDown (BytesPerLine * (Viewport.Height - 1 - (int)(Address - _displayStart) / BytesPerLine))
                    );
+        AddCommand (Command.DeleteCharLeft, () => true);
+        AddCommand (Command.DeleteCharRight, () => true);
+        AddCommand (Command.Insert, () => true);
 
         KeyBindings.Add (Key.CursorLeft, Command.Left);
         KeyBindings.Add (Key.CursorRight, Command.Right);
@@ -95,6 +102,10 @@ public class HexView : View, IDesignable
         KeyBindings.Add (Key.Tab, Command.Tab);
         KeyBindings.Add (Key.Tab.WithShift, Command.BackTab);
 
+        KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
+        KeyBindings.Add (Key.Delete, Command.DeleteCharRight);
+        KeyBindings.Add (Key.InsertChar, Command.Insert);
+
         KeyBindings.Remove (Key.Space);
         KeyBindings.Remove (Key.Enter);
 
@@ -111,8 +122,8 @@ public class HexView : View, IDesignable
     /// <value><c>true</c> to allow edits; otherwise, <c>false</c>.</value>
     public bool AllowEdits { get; set; } = true;
 
-    /// <summary>Gets the current cursor position.</summary>
-    public Point CursorPosition
+    /// <summary>Gets the current edit position.</summary>
+    public Point Position
     {
         get
         {
@@ -120,26 +131,16 @@ public class HexView : View, IDesignable
             {
                 return Point.Empty;
             }
-            var delta = (int)Address;
 
-            if (_leftSideHasFocus)
-            {
-                int line = delta / BytesPerLine;
-                int item = delta % BytesPerLine;
+            var delta = (int)Address;
 
-                return new (item, line);
-            }
-            else
-            {
-                int line = delta / BytesPerLine;
-                int item = delta % BytesPerLine;
+            int line = delta / BytesPerLine;
+            int item = delta % BytesPerLine;
 
-                return new (item, line);
-            }
+            return new (item, line);
         }
     }
 
-
     ///<inheritdoc/>
     public override Point? PositionCursor ()
     {
@@ -162,7 +163,6 @@ public class HexView : View, IDesignable
         return new (x, y);
     }
 
-
     private SortedDictionary<long, byte> _edits = [];
 
     /// <summary>
@@ -172,7 +172,6 @@ public class HexView : View, IDesignable
     /// <value>The edits.</value>
     public IReadOnlyDictionary<long, byte> Edits => _edits;
 
-
     private Stream? _source;
 
     /// <summary>
@@ -221,7 +220,6 @@ public class HexView : View, IDesignable
         }
     }
 
-
     private long _address;
 
     /// <summary>Gets or sets the current byte position in the <see cref="Stream"/>.</summary>
@@ -264,7 +262,7 @@ public class HexView : View, IDesignable
     private int _addressWidth = DEFAULT_ADDRESS_WIDTH;
 
     /// <summary>
-    /// Gets or sets the width of the Address column on the left. Set to 0 to hide. The default is 8.
+    ///     Gets or sets the width of the Address column on the left. Set to 0 to hide. The default is 8.
     /// </summary>
     public int AddressWidth
     {
@@ -275,15 +273,13 @@ public class HexView : View, IDesignable
             {
                 return;
             }
+
             _addressWidth = value;
             SetNeedsDisplay ();
         }
     }
 
-    private int GetLeftSideStartColumn ()
-    {
-        return AddressWidth == 0 ? 0 : AddressWidth + 1;
-    }
+    private int GetLeftSideStartColumn () { return AddressWidth == 0 ? 0 : AddressWidth + 1; }
 
     internal void SetDisplayStart (long value)
     {
@@ -303,7 +299,6 @@ public class HexView : View, IDesignable
         SetNeedsDisplay ();
     }
 
-
     /// <summary>
     ///     Applies and edits made to the <see cref="Stream"/> and resets the contents of the
     ///     <see cref="Edits"/> property.
@@ -461,7 +456,7 @@ public class HexView : View, IDesignable
             Move (0, line);
             currentAttribute = GetHotNormalColor ();
             Driver.SetAttribute (currentAttribute);
-            string address = $"{_displayStart + line * nblocks * NUM_BYTES_PER_HEX_COLUMN:x8}";
+            var address = $"{_displayStart + line * nblocks * NUM_BYTES_PER_HEX_COLUMN:x8}";
             Driver.AddStr ($"{address.Substring (8 - AddressWidth)}");
 
             if (AddressWidth > 0)
@@ -492,7 +487,7 @@ public class HexView : View, IDesignable
                     Driver.AddRune (_spaceCharRune);
                 }
 
-                Driver.AddStr (block + 1 == nblocks ? " " : "| ");
+                Driver.AddStr (block + 1 == nblocks ? " " : $"{_columnSeparatorRune} ");
             }
 
             for (var bitem = 0; bitem < nblocks * NUM_BYTES_PER_HEX_COLUMN; bitem++)
@@ -501,23 +496,37 @@ public class HexView : View, IDesignable
                 byte b = GetData (data, offset, out bool edited);
                 Rune c;
 
+                var utf8BytesConsumed = 0;
+
                 if (offset >= n && !edited)
                 {
                     c = _spaceCharRune;
                 }
                 else
                 {
-                    if (b < 32)
-                    {
-                        c = _periodCharRune;
-                    }
-                    else if (b > 127)
-                    {
-                        c = _periodCharRune;
-                    }
-                    else
+                    switch (b)
                     {
-                        Rune.DecodeFromUtf8 (new (ref b), out c, out _);
+                        //case < 32:
+                        //    c = _periodCharRune;
+
+                        //    break;
+                        case > 127:
+                            {
+                                var utf8 = GetData (data, offset, 4, out bool _);
+
+                                OperationStatus status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed);
+
+                                while (status == OperationStatus.NeedMoreData)
+                                {
+                                    status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed);
+                                }
+
+                                break;
+                            }
+                        default:
+                            Rune.DecodeFromUtf8 (new (ref b), out c, out _);
+
+                            break;
                     }
                 }
 
@@ -531,6 +540,12 @@ public class HexView : View, IDesignable
                 }
 
                 Driver.AddRune (c);
+
+                for (var i = 1; i < utf8BytesConsumed; i++)
+                {
+                    bitem++;
+                    Driver.AddRune (_periodCharRune);
+                }
             }
         }
 
@@ -547,7 +562,7 @@ public class HexView : View, IDesignable
     /// <summary>Raises the <see cref="Edited"/> event.</summary>
     protected void RaiseEdited (HexViewEditEventArgs e)
     {
-        OnEditied (e);
+        OnEdited (e);
         Edited?.Invoke (this, e);
     }
 
@@ -555,25 +570,27 @@ public class HexView : View, IDesignable
     public event EventHandler<HexViewEditEventArgs>? Edited;
 
     /// <summary>
-    /// 
     /// </summary>
     /// <param name="e"></param>
-    protected virtual void OnEditied (HexViewEditEventArgs e) { }
+    protected virtual void OnEdited (HexViewEditEventArgs e) { }
 
-    /// <summary>Raises the <see cref="PositionChanged"/> event.</summary>
+    /// <summary>
+    ///     Call this when <see cref="Position"/> (and <see cref="Address"/>) has changed. Raises the
+    ///     <see cref="PositionChanged"/> event.
+    /// </summary>
     protected void RaisePositionChanged ()
     {
-        HexViewEventArgs args = new (Address, CursorPosition, BytesPerLine);
+        HexViewEventArgs args = new (Address, Position, BytesPerLine);
         OnPositionChanged (args);
         PositionChanged?.Invoke (this, args);
     }
 
     /// <summary>
-    ///     Called when <see cref="Address"/> has changed.
+    ///     Called when <see cref="Position"/> (and <see cref="Address"/>) has changed.
     /// </summary>
     protected virtual void OnPositionChanged (HexViewEventArgs e) { }
 
-    /// <summary>Event to be invoked when the position and cursor position changes.</summary>
+    /// <summary>Raised when <see cref="Position"/> (and <see cref="Address"/>) has changed.</summary>
     public event EventHandler<HexViewEventArgs>? PositionChanged;
 
     /// <inheritdoc/>
@@ -584,17 +601,16 @@ public class HexView : View, IDesignable
             return false;
         }
 
-        // Ignore control characters and other special keys
-        if (keyEvent < Key.Space || keyEvent.KeyCode > KeyCode.CharMask)
-        {
-            return false;
-        }
-
         if (_leftSideHasFocus)
         {
             int value;
             var k = (char)keyEvent.KeyCode;
 
+            if (!char.IsAsciiDigit ((char)keyEvent.KeyCode))
+            {
+                return false;
+            }
+
             if (k is >= 'A' and <= 'F')
             {
                 value = k - 'A' + 10;
@@ -612,9 +628,7 @@ public class HexView : View, IDesignable
                 return false;
             }
 
-            byte b;
-
-            if (!_edits.TryGetValue (Address, out b))
+            if (!_edits.TryGetValue (Address, out byte b))
             {
                 _source.Position = Address;
                 b = (byte)_source.ReadByte ();
@@ -640,25 +654,40 @@ public class HexView : View, IDesignable
 
             return true;
         }
-        else
+
+        keyEvent = keyEvent.NoAlt.NoCtrl;
+        Rune r = keyEvent.AsRune;
+
+        if (Rune.IsControl (r))
         {
-            Rune r = keyEvent.AsRune;
+            return false;
+        }
 
-            // TODO: Enable entering Tab char - somehow disable Tab for navigation
+        var utf8 = new byte [4];
 
-            _edits [Address] = (byte)(r.Value & 0x00FF);
-            MoveRight ();
+        // If the rune is a wide char, encode as utf8
+        if (r.TryEncodeToUtf8 (utf8, out int bytesWritten))
+        {
+            if (bytesWritten > 1)
+            {
+                bytesWritten = 4;
+            }
 
-            if ((byte)(r.Value & 0xFF00) > 0)
+            for (var utfIndex = 0; utfIndex < bytesWritten; utfIndex++)
             {
-                _edits [Address] = (byte)(r.Value & 0xFF00);
+                _edits [Address] = utf8 [utfIndex];
+                RaiseEdited (new (Address, _edits [Address]));
                 MoveRight ();
             }
-
-            //RaiseEdited (new (Address, _edits [Address]));
+        }
+        else
+        {
+            _edits [Address] = (byte)r.Value;
+            RaiseEdited (new (Address, _edits [Address]));
+            MoveRight ();
         }
 
-        return false;
+        return true;
     }
 
     //
@@ -684,6 +713,31 @@ public class HexView : View, IDesignable
         return buffer [offset];
     }
 
+    private byte [] GetData (byte [] buffer, int offset, int count, out bool edited)
+    {
+        var returnBytes = new byte [count];
+        edited = false;
+
+        long pos = DisplayStart + offset;
+        for (long i = pos; i < pos + count; i++)
+        {
+            if (_edits.TryGetValue (i, out byte v))
+            {
+                edited = true;
+                returnBytes [i - pos] = v;
+            }
+            else
+            {
+                if (pos < buffer.Length - 1)
+                {
+                    returnBytes [i - pos] = buffer [pos];
+                }
+            }
+        }
+
+        return returnBytes;
+    }
+
     private void HexView_LayoutComplete (object? sender, LayoutEventArgs e)
     {
         // Small buffers will just show the position, with the bsize field value (4 bytes)
@@ -691,7 +745,9 @@ public class HexView : View, IDesignable
 
         if (Viewport.Width - GetLeftSideStartColumn () >= HEX_COLUMN_WIDTH)
         {
-            BytesPerLine = NUM_BYTES_PER_HEX_COLUMN * ((Viewport.Width - GetLeftSideStartColumn ()) / 18);
+            BytesPerLine = Math.Max (
+                                     NUM_BYTES_PER_HEX_COLUMN,
+                                     NUM_BYTES_PER_HEX_COLUMN * ((Viewport.Width - GetLeftSideStartColumn ()) / (HEX_COLUMN_WIDTH + NUM_BYTES_PER_HEX_COLUMN)));
         }
     }
 
@@ -699,8 +755,9 @@ public class HexView : View, IDesignable
     {
         RedisplayLine (Address);
 
-        if (Address + bytes < _source.Length)
+        if (Address + bytes < GetEditedSize ())
         {
+            // We can move down lines cleanly (without extending stream)
             Address += bytes;
         }
         else if ((bytes == BytesPerLine * Viewport.Height && _source.Length >= DisplayStart + BytesPerLine * Viewport.Height)
@@ -709,7 +766,8 @@ public class HexView : View, IDesignable
         {
             long p = Address;
 
-            while (p + BytesPerLine < _source.Length)
+            // This lets address go past the end of the stream one, enabling adding to the stream.
+            while (p + BytesPerLine <= GetEditedSize ())
             {
                 p += BytesPerLine;
             }
@@ -732,7 +790,8 @@ public class HexView : View, IDesignable
 
     private bool MoveEnd ()
     {
-        Address = _source!.Length;
+        // This lets address go past the end of the stream one, enabling adding to the stream.
+        Address = GetEditedSize ();
 
         if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
         {
@@ -749,7 +808,8 @@ public class HexView : View, IDesignable
 
     private bool MoveEndOfLine ()
     {
-        Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, _source!.Length);
+        // This lets address go past the end of the stream one, enabling adding to the stream.
+        Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, GetEditedSize ());
         SetNeedsDisplay ();
 
         return true;
@@ -815,7 +875,8 @@ public class HexView : View, IDesignable
             _firstNibble = true;
         }
 
-        if (Address < _source.Length - 1)
+        // This lets address go past the end of the stream one, enabling adding to the stream.
+        if (Address < GetEditedSize ())
         {
             Address++;
         }
@@ -833,6 +894,18 @@ public class HexView : View, IDesignable
         return true;
     }
 
+    private long GetEditedSize ()
+    {
+        if (_edits.Count == 0)
+        {
+            return _source!.Length;
+        }
+
+        long maxEditAddress = _edits.Keys.Max ();
+
+        return Math.Max (_source!.Length, maxEditAddress + 1);
+    }
+
     private bool MoveLeftStart ()
     {
         Address = Address / BytesPerLine * BytesPerLine;

+ 6 - 6
Terminal.Gui/Views/HexViewEventArgs.cs

@@ -13,20 +13,20 @@ public class HexViewEventArgs : EventArgs
 {
     /// <summary>Initializes a new instance of <see cref="HexViewEventArgs"/></summary>
     /// <param name="address">The byte position in the steam.</param>
-    /// <param name="cursor">The cursor position.</param>
+    /// <param name="position">The edit position.</param>
     /// <param name="lineLength">Line bytes length.</param>
-    public HexViewEventArgs (long address, Point cursor, int lineLength)
+    public HexViewEventArgs (long address, Point position, int lineLength)
     {
         Address = address;
-        CursorPosition = cursor;
+        Position = position;
         BytesPerLine = lineLength;
     }
 
     /// <summary>The bytes length per line.</summary>
     public int BytesPerLine { get; private set; }
 
-    /// <summary>Gets the current cursor position starting at one for both, line and column.</summary>
-    public Point CursorPosition { get; private set; }
+    /// <summary>Gets the current edit position.</summary>
+    public Point Position { get; private set; }
 
     /// <summary>Gets the byte position in the <see cref="Stream"/>.</summary>
     public long Address { get; private set; }
@@ -47,6 +47,6 @@ public class HexViewEditEventArgs : EventArgs
     /// <summary>Gets the new value for that <see cref="Address"/>.</summary>
     public byte NewValue { get; }
 
-    /// <summary>Gets the adress of the edit in the stream.</summary>
+    /// <summary>Gets the address of the edit in the stream.</summary>
     public long Address { get; }
 }

+ 2 - 2
UICatalog/Scenarios/HexEditor.cs

@@ -39,7 +39,7 @@ public class HexEditor : Scenario
             Width = Dim.Fill (),
             Height = Dim.Fill (1),
             Title = _fileName ?? "Untitled",
-            BorderStyle = LineStyle.Rounded
+            BorderStyle = LineStyle.Rounded,
         };
         _hexView.Edited += _hexView_Edited;
         _hexView.PositionChanged += _hexView_PositionChanged;
@@ -161,7 +161,7 @@ public class HexEditor : Scenario
         _scInfo.Title =
             $"Bytes: {_hexView.Source!.Length}";
         _scPosition.Title =
-            $"L: {obj.CursorPosition.Y} C: {obj.CursorPosition.X} Per Line: {obj.BytesPerLine}";
+            $"L: {obj.Position.Y} C: {obj.Position.X} Per Line: {obj.BytesPerLine}";
 
         if (_scAddress.CommandView is NumericUpDown<long> addrNumericUpDown)
         {

+ 1 - 1
UICatalog/Scenarios/Text.cs

@@ -177,7 +177,7 @@ public class Text : Scenario
                          new MemoryStream (Encoding.UTF8.GetBytes ("HexEditor Unicode that shouldn't 𝔹Aℝ𝔽!"))
                         )
             {
-                X = Pos.Right (label) + 1, Y = Pos.Bottom (chxMultiline) + 1, Width = Dim.Percent (50) - 1, Height = Dim.Percent (30)
+                X = Pos.Right (label) + 1, Y = Pos.Bottom (chxMultiline) + 1, Width = Dim.Percent (50) - 1, Height = Dim.Percent (30),
             };
         win.Add (hexEditor);
 

+ 49 - 127
UnitTests/Views/HexViewTests.cs

@@ -8,39 +8,25 @@ public class HexViewTests
 {
     [Theory]
     [InlineData (0, 4)]
-    [InlineData (9, 4)]
-    [InlineData (20, 4)]
-    [InlineData (24, 4)]
-    [InlineData (30, 4)]
-    [InlineData (31, 4)]
-    [InlineData (32, 4)]
-    [InlineData (33, 4)]
-    [InlineData (34, 4)]
+    [InlineData (4, 4)]
+    [InlineData (8, 4)]
     [InlineData (35, 4)]
-    [InlineData (36, 4)]
-    [InlineData (37, 4)]
-    [InlineData (50, 8)]
-    [InlineData (51, 8)]
+    [InlineData (36, 8)]
+    [InlineData (37, 8)]
+    [InlineData (41, 8)]
+    [InlineData (54, 12)]
+    [InlineData (55, 12)]
+    [InlineData (71, 12)]
+    [InlineData (72, 16)]
+    [InlineData (73, 16)]
     public void BytesPerLine_Calculates_Correctly (int width, int expectedBpl)
     {
-        var hv = new HexView (LoadStream (null, out long _)) { Width = width, Height = 10 };
+        var hv = new HexView (LoadStream (null, out long _)) { Width = width, Height = 10, AddressWidth = 0 };
         hv.LayoutSubviews ();
 
         Assert.Equal (expectedBpl, hv.BytesPerLine);
     }
 
-    [Theory]
-    [InlineData ("01234", 20, 4)]
-    [InlineData ("012345", 20, 4)]
-    public void xuz (string str, int width, int expectedBpl)
-    {
-        var hv = new HexView (LoadStream (str, out long _)) { Width = width, Height = 10 };
-        hv.LayoutSubviews ();
-
-        Assert.Equal (expectedBpl, hv.BytesPerLine);
-    }
-
-
     [Fact]
     public void AllowEdits_Edits_ApplyEdits ()
     {
@@ -54,7 +40,7 @@ public class HexViewTests
         Assert.True (hv.NewKeyDownEvent (Key.Home));
         Assert.False (hv.NewKeyDownEvent (Key.A));
         Assert.Empty (hv.Edits);
-        Assert.Equal (126, hv.Source.Length);
+        Assert.Equal (126, hv.Source!.Length);
 
         hv.AllowEdits = true;
         Assert.True (hv.NewKeyDownEvent (Key.D4));
@@ -131,7 +117,7 @@ public class HexViewTests
     }
 
     [Fact]
-    public void CursorPosition_Encoding_Default ()
+    public void Position_Encoding_Default ()
     {
         var hv = new HexView (LoadStream (null, out _)) { Width = 100, Height = 100 };
         Application.Top = new Toplevel ();
@@ -139,59 +125,64 @@ public class HexViewTests
 
         Application.Top.LayoutSubviews ();
 
-        Assert.Equal (new (0, 0), hv.CursorPosition);
+        Assert.Equal (63, hv.Source!.Length);
         Assert.Equal (20, hv.BytesPerLine);
 
+        Assert.Equal (new (0, 0), hv.Position);
+
         Assert.True (hv.NewKeyDownEvent (Key.Tab));
-        Assert.Equal (new (0, 0), hv.CursorPosition);
+        Assert.Equal (new (0, 0), hv.Position);
 
         Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
-        Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine - 1);
+        Assert.Equal (hv.BytesPerLine - 1, hv.Position.X);
+
         Assert.True (hv.NewKeyDownEvent (Key.Home));
 
         Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (new (1, 0), hv.CursorPosition);
+        Assert.Equal (new (1, 0), hv.Position);
 
         Assert.True (hv.NewKeyDownEvent (Key.CursorDown));
-        Assert.Equal (new (1, 1), hv.CursorPosition);
+        Assert.Equal (new (1, 1), hv.Position);
 
         Assert.True (hv.NewKeyDownEvent (Key.End));
-        Assert.Equal (new (2, 2), hv.CursorPosition);
-        int col = hv.CursorPosition.X;
-        int line = hv.CursorPosition.Y;
-        int offset = line * (hv.BytesPerLine - col);
-        Assert.Equal (hv.Address, col * line + offset);
+        Assert.Equal (new (3, 3), hv.Position);
+
+        Assert.Equal (hv.Source!.Length, hv.Address);
         Application.Top.Dispose ();
         Application.ResetState (true);
     }
 
     [Fact]
-    public void CursorPosition_Encoding_Unicode ()
+    public void Position_Encoding_Unicode ()
     {
-        var hv = new HexView (LoadStream (null, out _, true)) { Width = Dim.Fill (), Height = Dim.Fill () };
+        var hv = new HexView (LoadStream (null, out _, unicode: true)) { Width = 100, Height = 100 };
         Application.Top = new Toplevel ();
         Application.Top.Add (hv);
 
         hv.LayoutSubviews ();
 
-        Assert.Equal (new (0, 0), hv.CursorPosition);
+        Assert.Equal (126, hv.Source!.Length);
+        Assert.Equal (20, hv.BytesPerLine);
+
+        Assert.Equal (new (0, 0), hv.Position);
 
         Assert.True (hv.NewKeyDownEvent (Key.Tab));
+
         Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
-        Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine);
+        Assert.Equal (hv.BytesPerLine - 1, hv.Position.X);
+
         Assert.True (hv.NewKeyDownEvent (Key.Home));
 
         Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (new (2, 1), hv.CursorPosition);
+        Assert.Equal (new (1, 0), hv.Position);
 
         Assert.True (hv.NewKeyDownEvent (Key.CursorDown));
-        Assert.Equal (new (2, 2), hv.CursorPosition);
+        Assert.Equal (new (1, 1), hv.Position);
 
         Assert.True (hv.NewKeyDownEvent (Key.End));
-        int col = hv.CursorPosition.X;
-        int line = hv.CursorPosition.Y;
-        int offset = (line - 1) * (hv.BytesPerLine - col);
-        Assert.Equal (hv.Address, col * line + offset);
+        Assert.Equal (new (6, 6), hv.Position);
+
+        Assert.Equal (hv.Source!.Length, hv.Address);
         Application.Top.Dispose ();
         Application.ResetState (true);
     }
@@ -271,7 +262,7 @@ public class HexViewTests
 
         hv.LayoutSubviews ();
 
-        Assert.Equal (MEM_STRING_LENGTH, hv.Source.Length);
+        Assert.Equal (MEM_STRING_LENGTH, hv.Source!.Length);
         Assert.Equal (0, hv.Address);
         Assert.Equal (4, hv.BytesPerLine);
 
@@ -317,75 +308,6 @@ public class HexViewTests
         Application.ResetState (true);
     }
 
-    [Fact]
-    public void Position_Using_Encoding_Default ()
-    {
-        var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 20 };
-        hv.LayoutSubviews ();
-
-        // Needed because HexView relies on LayoutComplete to calc sizes
-        hv.LayoutSubviews ();
-        Assert.Equal (MEM_STRING_LENGTH, hv.Source.Length);
-        Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position);
-        Assert.Equal (0, hv.Address);
-
-        // left side needed to press twice
-        Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position);
-        Assert.Equal (1, hv.Address);
-        Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position);
-        Assert.Equal (2, hv.Address);
-
-        // right side only needed to press one time
-        Assert.True (hv.NewKeyDownEvent (Key.Tab));
-        Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position);
-        Assert.Equal (2, hv.Address);
-        Assert.True (hv.NewKeyDownEvent (Key.CursorLeft));
-        Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position);
-        Assert.Equal (1, hv.Address);
-
-        // last position is equal to the source length
-        Assert.True (hv.NewKeyDownEvent (Key.End));
-        Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position);
-        Assert.Equal (64, hv.Address);
-        Assert.Equal (hv.Address - 1, hv.Source.Length);
-    }
-
-    [Fact]
-    public void Position_Using_Encoding_Unicode ()
-    {
-        var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 };
-
-        // Needed because HexView relies on LayoutComplete to calc sizes
-        hv.LayoutSubviews ();
-        Assert.Equal (126, hv.Source.Length);
-        Assert.Equal (126, hv.Source.Position);
-        Assert.Equal (1, hv.Address);
-
-        // left side needed to press twice
-        Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (126, hv.Source.Position);
-        Assert.Equal (1, hv.Address);
-        Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (126, hv.Source.Position);
-        Assert.Equal (2, hv.Address);
-
-        // right side only needed to press one time
-        Assert.True (hv.NewKeyDownEvent (Key.Tab));
-        Assert.Equal (126, hv.Source.Position);
-        Assert.Equal (2, hv.Address);
-        Assert.True (hv.NewKeyDownEvent (Key.CursorLeft));
-        Assert.Equal (126, hv.Source.Position);
-        Assert.Equal (1, hv.Address);
-
-        // last position is equal to the source length
-        Assert.True (hv.NewKeyDownEvent (Key.End));
-        Assert.Equal (126, hv.Source.Position);
-        Assert.Equal (127, hv.Address);
-        Assert.Equal (hv.Address - 1, hv.Source.Length);
-    }
-
     [Fact]
     public void PositionChanged_Event ()
     {
@@ -398,15 +320,15 @@ public class HexViewTests
         HexViewEventArgs hexViewEventArgs = null;
         hv.PositionChanged += (s, e) => hexViewEventArgs = e;
 
-        Assert.Equal (12, hv.BytesPerLine);
+        Assert.Equal (4, hv.BytesPerLine);
 
         Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); // left side must press twice
         Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
         Assert.True (hv.NewKeyDownEvent (Key.CursorDown));
 
-        Assert.Equal (12, hexViewEventArgs.BytesPerLine);
-        Assert.Equal (new (2, 2), hexViewEventArgs.CursorPosition);
-        Assert.Equal (14, hexViewEventArgs.Address);
+        Assert.Equal (4, hexViewEventArgs.BytesPerLine);
+        Assert.Equal (new (1, 1), hexViewEventArgs.Position);
+        Assert.Equal (5, hexViewEventArgs.Address);
         Application.Top.Dispose ();
         Application.ResetState (true);
     }
@@ -421,27 +343,27 @@ public class HexViewTests
         hv.LayoutSubviews ();
 
         Assert.True (hv.NewKeyDownEvent (Key.End));
-        Assert.Equal (62, hv.DisplayStart);
-        Assert.Equal (64, hv.Address);
+        Assert.Equal (MEM_STRING_LENGTH - 1, hv.DisplayStart);
+        Assert.Equal (MEM_STRING_LENGTH, hv.Address);
 
         hv.Source = new MemoryStream ();
         Assert.Equal (0, hv.DisplayStart);
-        Assert.Equal (0, hv.Address - 1);
+        Assert.Equal (0, hv.Address);
 
         hv.Source = LoadStream (null, out _);
         hv.Width = Dim.Fill ();
         hv.Height = Dim.Fill ();
         Application.Top.LayoutSubviews ();
         Assert.Equal (0, hv.DisplayStart);
-        Assert.Equal (0, hv.Address - 1);
+        Assert.Equal (0, hv.Address);
 
         Assert.True (hv.NewKeyDownEvent (Key.End));
         Assert.Equal (0, hv.DisplayStart);
-        Assert.Equal (64, hv.Address);
+        Assert.Equal (MEM_STRING_LENGTH, hv.Address);
 
         hv.Source = new MemoryStream ();
         Assert.Equal (0, hv.DisplayStart);
-        Assert.Equal (0, hv.Address - 1);
+        Assert.Equal (0, hv.Address);
         Application.Top.Dispose ();
         Application.ResetState (true);
     }

+ 4 - 4
docfx/docs/cursor.md

@@ -4,19 +4,19 @@ See end for list of issues this design addresses.
 
 ## Tenets for Cursor Support (Unless you know better ones...)
 
-1. **More GUI than Command Line**. The concept of a cursor on the command line of a terminal is intrinsically tied to enabling the user to know where keybaord import is going to impact text editing. TUI apps have many more modalities than text editing where the keyboard is used (e.g. scrolling through a `ColorPicker`). Terminal.Gui's cursor system is biased towards the broader TUI experiences.
+1. **More GUI than Command Line**. The concept of a cursor on the command line of a terminal is intrinsically tied to enabling the user to know where keyboard import is going to impact text editing. TUI apps have many more modalities than text editing where the keyboard is used (e.g. scrolling through a `ColorPicker`). Terminal.Gui's cursor system is biased towards the broader TUI experiences.
 
 2. **Be Consistent With the User's Platform** - Users get to choose the platform they run *Terminal.Gui* apps on and the cursor should behave in a way consistent with the terminal.
 
 ## Lexicon & Taxonomy
 
 - Navigation - Refers to the user-experience for moving Focus between views in the application view-hierarchy. See [Navigation](navigation.md) for a deep-dive.
-- Focus - Indicates which View in the view-hierarchy is currently the one receiving keyboard input. Only one view-heirachy in an applicstion can have focus (`view.HasFocus == true`), and there is only one View in a focused heirarchy that is the most-focused; the one recieving keyboard input. See [Navigation](navigation.md) for a deep-dive.
-- Cursor - A visual indicator to the user where keyboard input will have an impact. There is one Cursor per terminal sesssion.
+- Focus - Indicates which View in the view-hierarchy is currently the one receiving keyboard input. Only one view-hexarchy in an application can have focus (`view.HasFocus == true`), and there is only one View in a focused hierarchy that is the most-focused; the one receiving keyboard input. See [Navigation](navigation.md) for a deep-dive.
+- Cursor - A visual indicator to the user where keyboard input will have an impact. There is one Cursor per terminal session.
 - Cursor Location - The top-left corner of the Cursor. In text entry scenarios, new text will be inserted to the left/top of the Cursor Location. 
 - Cursor Size - The width and height of the cursor. Currently the size is limited to 1x1.
 - Cursor Style - How the cursor renders. Some terminals support various cursor styles such as Block and Underline.
-- Cursor Visibilty - Whether the cursor is visible to the user or not. NOTE: Some ConsoleDrivers overload Cursor Style and Cursor Visibility, making "invisible" a style. Terminal.Gui HIDES this from developers and changing the visibilty of the cursor does NOT change the style.
+- Cursor Visibility - Whether the cursor is visible to the user or not. NOTE: Some ConsoleDrivers overload Cursor Style and Cursor Visibility, making "invisible" a style. Terminal.Gui HIDES this from developers and changing the visibility of the cursor does NOT change the style.
 - Caret - Visual indicator that  where text entry will occur. 
 - Selection - A visual indicator to the user that something is selected. It is common for the Selection and Cursor to be the same. It is also common for the Selection and Cursor to be distinct. In a `ListView` the Cursor and Selection (`SelectedItem`) are the same, but the `Cursor` is not visible. In a `TextView` with text selected, the `Cursor` is at either the start or end of the `Selection`. A `TableView' supports mutliple things being selected at once.