From fc818b02740201a6b8881e583afd24d2c9dfec22 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 12 Nov 2025 17:22:51 +0000 Subject: [PATCH] Fixes #4382. StringExtensions.GetColumns method should only return the total text width and not the sum of all runes width (#4383) * Fixes #4382. StringExtensions.GetColumns method should only return the total text width and not the sum of all runes width * Trying to fix unit test error * Update StringExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add unit test to prove that null and empty string doesn't not throws anything. --------- Co-authored-by: Tig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Terminal.Gui/Text/StringExtensions.cs | 25 ++++++++++++++- Tests/UnitTests/Views/HexViewTests.cs | 3 +- .../UnitTestsParallelizable/Text/RuneTests.cs | 11 ++++--- .../Text/StringTests.cs | 32 ++++++++++++++++--- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/Terminal.Gui/Text/StringExtensions.cs b/Terminal.Gui/Text/StringExtensions.cs index a38bb5799..e379cf3da 100644 --- a/Terminal.Gui/Text/StringExtensions.cs +++ b/Terminal.Gui/Text/StringExtensions.cs @@ -1,5 +1,6 @@ ๏ปฟ#nullable enable using System.Buffers; +using System.Globalization; namespace Terminal.Gui.Text; @@ -55,7 +56,29 @@ public static class StringExtensions /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The string to measure. /// - public static int GetColumns (this string str) { return str is null ? 0 : str.EnumerateRunes ().Sum (r => Math.Max (r.GetColumns (), 0)); } + public static int GetColumns (this string str) + { + if (string.IsNullOrEmpty (str)) + { + return 0; + } + + var total = 0; + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (str); + + while (enumerator.MoveNext ()) + { + string element = enumerator.GetTextElement (); + + // Get the maximum rune width within this grapheme cluster + int width = element + .EnumerateRunes () + .Max (r => Math.Max (r.GetColumns (), 0)); + total += width; + } + + return total; + } /// Gets the number of runes in the string. /// This is a Terminal.Gui extension method to to support TUI text manipulation. diff --git a/Tests/UnitTests/Views/HexViewTests.cs b/Tests/UnitTests/Views/HexViewTests.cs index aafd21aeb..83d661f6f 100644 --- a/Tests/UnitTests/Views/HexViewTests.cs +++ b/Tests/UnitTests/Views/HexViewTests.cs @@ -72,7 +72,6 @@ public class HexViewTests Application.Top.Dispose (); Application.ResetState (true); - } [Fact] @@ -321,6 +320,7 @@ public class HexViewTests [Fact] public void PositionChanged_Event () { + Application.Navigation = new ApplicationNavigation (); var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; Application.Top = new Toplevel (); Application.Top.Add (hv); @@ -346,6 +346,7 @@ public class HexViewTests [Fact] public void Source_Sets_Address_To_Zero_If_Greater_Than_Source_Length () { + Application.Navigation = new ApplicationNavigation (); var hv = new HexView (LoadStream (null, out _)) { Width = 10, Height = 5 }; Application.Top = new Toplevel (); Application.Top.Add (hv); diff --git a/Tests/UnitTestsParallelizable/Text/RuneTests.cs b/Tests/UnitTestsParallelizable/Text/RuneTests.cs index d37a94016..34214d6ef 100644 --- a/Tests/UnitTestsParallelizable/Text/RuneTests.cs +++ b/Tests/UnitTestsParallelizable/Text/RuneTests.cs @@ -7,7 +7,7 @@ namespace UnitTests_Parallelizable.TextTests; public class RuneTests { [Fact] - public void Cast_To_Char_Durrogate_Pair_Return_UTF16 () + public void Cast_To_Char_Surrogate_Pair_Return_UTF16 () { Assert.NotEqual ("๐”น", $"{new Rune (unchecked ((char)0x1d539))}"); Assert.Equal ("ํ”น", $"{new Rune (unchecked ((char)0x1d539))}"); @@ -65,8 +65,11 @@ public class RuneTests PrintTextElementCount ("\u0061\u0301", "aฬ", 1, 2, 2, 1); PrintTextElementCount ("\u0061\u0301", "aฬ", 1, 2, 2, 1); PrintTextElementCount ("\u0065\u0301", "eฬ", 1, 2, 2, 1); - PrintTextElementCount ("\U0001f469\U0001f3fd\u200d\U0001f692", "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿš’", 6, 4, 7, 1); + PrintTextElementCount ("\U0001f469\U0001f3fd\u200d\U0001f692", "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿš’", 2, 4, 7, 1); PrintTextElementCount ("\ud801\udccf", "๐“", 1, 1, 2, 1); + PrintTextElementCount ("\U0001F468\u200D\U0001F469\u200D\U0001F467\u200D\U0001F466", "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", 2, 7, 11, 1); + PrintTextElementCount ("\U0001f469\u200d\U0001f692", "๐Ÿ‘ฉโ€๐Ÿš’", 2, 3, 5, 1); + PrintTextElementCount ("\u0068\u0069", "hi", 2, 2, 2, 2); } [Theory] @@ -84,8 +87,8 @@ public class RuneTests 2, 1 )] // the letters แ„‡แ…ฅแ†ธ join to form the Korean word for "rice:" U+BC95 ๋ฒ• (read from top left to bottom right) - [InlineData ("\U0001F468\u200D\U0001F469\u200D\U0001F467", "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง", 8, 6, 8)] // Man, Woman and Girl emoji. - [InlineData ("\u0915\u093f", "เค•เคฟ", 2, 2, 2)] // Hindi เค•เคฟ with DEVANAGARI LETTER KA and DEVANAGARI VOWEL SIGN I + [InlineData ("\U0001F468\u200D\U0001F469\u200D\U0001F467", "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง", 8, 2, 8)] // Man, Woman and Girl emoji. + [InlineData ("\u0915\u093f", "เค•เคฟ", 2, 1, 2)] // Hindi เค•เคฟ with DEVANAGARI LETTER KA and DEVANAGARI VOWEL SIGN I [InlineData ( "\u0e4d\u0e32", "เนเธฒ", diff --git a/Tests/UnitTestsParallelizable/Text/StringTests.cs b/Tests/UnitTestsParallelizable/Text/StringTests.cs index c2dc80224..80c52b96e 100644 --- a/Tests/UnitTestsParallelizable/Text/StringTests.cs +++ b/Tests/UnitTestsParallelizable/Text/StringTests.cs @@ -33,11 +33,11 @@ public class StringTests [InlineData ("๐Ÿ™‚", 2)] [InlineData ("a๐Ÿ™‚", 3)] [InlineData ("๐Ÿ™‚a", 3)] - [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", 8)] - [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ™‚", 10)] - [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ™‚a", 11)] - [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆa๐Ÿ™‚", 11)] - [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", 16)] + [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", 2)] + [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ™‚", 4)] + [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ™‚a", 5)] + [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆa๐Ÿ™‚", 5)] + [InlineData ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", 4)] [InlineData ("ๅฑฑ", 2)] // The character for "mountain" in Chinese/Japanese/Korean (ๅฑฑ), Unicode U+5C71 [InlineData ("ๅฑฑ๐Ÿ™‚", 4)] // The character for "mountain" in Chinese/Japanese/Korean (ๅฑฑ), Unicode U+5C71 //[InlineData ("\ufe20\ufe21", 2)] // Combining Ligature Left Half ๏ธ  - U+fe20 -https://github.com/microsoft/terminal/blob/main/src/types/unicode_width_overrides.xml @@ -57,4 +57,26 @@ public class StringTests var str = "a"; Assert.Equal (1, str.GetColumns ()); } + + [Fact] + public void TestGetColumns_Zero_Width () + { + var str = "\u200D"; + Assert.Equal (0, str.GetColumns ()); + } + + [Theory] + [InlineData (null)] + [InlineData ("")] + public void TestGetColumns_Does_Not_Throws_With_Null_And_Empty_String (string? text) + { + if (text is null) + { + Assert.Equal (0, StringExtensions.GetColumns (text!)); + } + else + { + Assert.Equal (0, text.GetColumns ()); + } + } }