From b83bcc2fdb16c3ee342cad9d7b2af6bdfec9aec0 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 15 Oct 2025 11:54:21 -0600 Subject: [PATCH] Fixes #4259. Our wcwidth library is out of date (#4281) * Update package versions and remove hack code from RuneExtensions Updated several package versions in `Directory.Packages.props`, including `JetBrains.Annotations`, `Microsoft.Extensions.Logging.Abstractions`, `System.IO.Abstractions`, and `Wcwidth`. Refactored methods in `RuneExtensions.cs`: - Simplified `GetColumns` by removing special Unicode handling. - Renamed constants to follow naming conventions. - Improved logic and readability in `DecodeSurrogatePair`, `Encode`, and `GetEncodingLength`. - Streamlined `IsSurrogatePair` and `MakePrintable` for clarity and efficiency. * Update package version ranges for flexibility Updated the `JetBrains.Annotations` package to use a version range starting from `2025.2.2` to allow future updates. Adjusted the `Microsoft.Extensions.Logging.Abstractions` package to a version range `[9.0.0,10)` for compatibility. Changed `System.IO.Abstractions` to a range `[22.0.16,23)` and `Wcwidth` to `[3.0.0,)` to enable future updates within specified ranges. --- Directory.Packages.props | 8 ++-- Terminal.Gui/Text/RuneExtensions.cs | 63 ++++++++++++++--------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 836970b60..d62a1d298 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,14 +11,14 @@ - + - + - - + + diff --git a/Terminal.Gui/Text/RuneExtensions.cs b/Terminal.Gui/Text/RuneExtensions.cs index 70f68b339..88718bc33 100644 --- a/Terminal.Gui/Text/RuneExtensions.cs +++ b/Terminal.Gui/Text/RuneExtensions.cs @@ -37,22 +37,27 @@ public static class RuneExtensions public static bool DecodeSurrogatePair (this Rune rune, out char []? chars) { bool isSingleUtf16CodeUnit = rune.IsBmp; + if (isSingleUtf16CodeUnit) { chars = null; + return false; } - const int maxCharsPerRune = 2; - Span charBuffer = stackalloc char[maxCharsPerRune]; + const int MAX_CHARS_PER_RUNE = 2; + Span charBuffer = stackalloc char [MAX_CHARS_PER_RUNE]; int charsWritten = rune.EncodeToUtf16 (charBuffer); + if (charsWritten >= 2 && char.IsSurrogatePair (charBuffer [0], charBuffer [1])) { chars = charBuffer [..charsWritten].ToArray (); + return true; } chars = null; + return false; } @@ -65,23 +70,26 @@ public static class RuneExtensions /// he number of bytes written into the destination buffer. public static int Encode (this Rune rune, byte [] dest, int start = 0, int count = -1) { - const int maxUtf8BytesPerRune = 4; - Span bytes = stackalloc byte[maxUtf8BytesPerRune]; + const int MAX_UTF8_BYTES_PER_RUNE = 4; + Span bytes = stackalloc byte [MAX_UTF8_BYTES_PER_RUNE]; int writtenBytes = rune.EncodeToUtf8 (bytes); int bytesToCopy = count == -1 - ? writtenBytes - : Math.Min (count, writtenBytes); - int bytesWritten = 0; - for (int i = 0; i < bytesToCopy; i++) + ? writtenBytes + : Math.Min (count, writtenBytes); + var bytesWritten = 0; + + for (var i = 0; i < bytesToCopy; i++) { if (bytes [i] == '\0') { break; } + dest [start + i] = bytes [i]; bytesWritten++; } + return bytesWritten; } @@ -111,22 +119,7 @@ public static class RuneExtensions /// The number of columns required to fit the rune, 0 if the argument is the null character, or -1 if the value is /// not printable, otherwise the number of columns that the rune occupies. /// - public static int GetColumns (this Rune rune) - { - int value = rune.Value; - - // TODO: Remove this code when #4259 is fixed - // TODO: See https://github.com/gui-cs/Terminal.Gui/issues/4259 - if (value is >= 0x2630 and <= 0x2637 || // Trigrams - value is >= 0x268A and <= 0x268F || // Monograms/Digrams - value is >= 0x4DC0 and <= 0x4DFF) // Hexagrams - { - return 2; // Assume double-width due to Windows Terminal font rendering - } - - // Fallback to original GetWidth for other code points - return UnicodeCalculator.GetWidth (rune); - } + public static int GetColumns (this Rune rune) { return UnicodeCalculator.GetWidth (rune); } /// Get number of bytes required to encode the rune, based on the provided encoding. /// This is a Terminal.Gui extension method to to support TUI text manipulation. @@ -137,21 +130,23 @@ public static class RuneExtensions { encoding ??= Encoding.UTF8; - const int maxCharsPerRune = 2; + const int MAX_CHARS_PER_RUNE = 2; + // Get characters with UTF16 to keep that part independent of selected encoding. - Span charBuffer = stackalloc char[maxCharsPerRune]; - int charsWritten = rune.EncodeToUtf16(charBuffer); - Span chars = charBuffer[..charsWritten]; + Span charBuffer = stackalloc char [MAX_CHARS_PER_RUNE]; + int charsWritten = rune.EncodeToUtf16 (charBuffer); + Span chars = charBuffer [..charsWritten]; int maxEncodedLength = encoding.GetMaxByteCount (charsWritten); - Span byteBuffer = stackalloc byte[maxEncodedLength]; + Span byteBuffer = stackalloc byte [maxEncodedLength]; int bytesEncoded = encoding.GetBytes (chars, byteBuffer); - ReadOnlySpan encodedBytes = byteBuffer[..bytesEncoded]; + ReadOnlySpan encodedBytes = byteBuffer [..bytesEncoded]; if (encodedBytes [^1] == '\0') { return encodedBytes.Length - 1; } + return encodedBytes.Length; } @@ -175,14 +170,16 @@ public static class RuneExtensions public static bool IsSurrogatePair (this Rune rune) { bool isSingleUtf16CodeUnit = rune.IsBmp; + if (isSingleUtf16CodeUnit) { return false; } - const int maxCharsPerRune = 2; - Span charBuffer = stackalloc char[maxCharsPerRune]; + const int MAX_CHARS_PER_RUNE = 2; + Span charBuffer = stackalloc char [MAX_CHARS_PER_RUNE]; int charsWritten = rune.EncodeToUtf16 (charBuffer); + return charsWritten >= 2 && char.IsSurrogatePair (charBuffer [0], charBuffer [1]); } @@ -193,5 +190,5 @@ public static class RuneExtensions /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// /// - public static Rune MakePrintable (this Rune rune) { return Rune.IsControl (rune) ? new Rune (rune.Value + 0x2400) : rune; } + public static Rune MakePrintable (this Rune rune) { return Rune.IsControl (rune) ? new (rune.Value + 0x2400) : rune; } }