From 77fbb68a8058fa54561daa6a2b1579003a4422b0 Mon Sep 17 00:00:00 2001 From: Norman Dankert Date: Fri, 7 Feb 2025 23:31:54 +0100 Subject: [PATCH 1/3] Fix handling of surrogate pairs in length calculation --- PSReadLine/Render.Helper.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 8fc56bea1..8fa9ca642 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -57,7 +57,13 @@ internal static int LengthInBufferCells(string str, int start, int end) for (var i = start; i < end; i++) { var c = str[i]; - if (c == 0x1b && (i+1) < end && str[i+1] == '[') + if (char.IsHighSurrogate(c) && (i + 1) < end && char.IsLowSurrogate(str[i + 1])) + { + sum++; + i++; // Skip the low surrogate + continue; + } + else if (c == 0x1b && (i + 1) < end && str[i + 1] == '[') { // Simple escape sequence skipping i += 2; @@ -77,7 +83,13 @@ internal static int LengthInBufferCells(StringBuilder sb, int start, int end) for (var i = start; i < end; i++) { var c = sb[i]; - if (c == 0x1b && (i + 1) < end && sb[i + 1] == '[') + if (char.IsHighSurrogate(c) && (i + 1) < end && char.IsLowSurrogate(sb[i + 1])) + { + sum++; + i++; // Skip the low surrogate + continue; + } + else if (c == 0x1b && (i + 1) < end && sb[i + 1] == '[') { // Simple escape sequence skipping i += 2; From 02f3b4c8e11479a23a97e4aa8ee97b061cd98de0 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:49:43 -0700 Subject: [PATCH 2/3] Add Claude generated unit tests I reviewed and they seem to correctly cover the reported (and solved) issues with glyphs / surrogate pairs. --- test/RenderTest.cs | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/RenderTest.cs b/test/RenderTest.cs index a1222c115..3de61cc02 100644 --- a/test/RenderTest.cs +++ b/test/RenderTest.cs @@ -331,5 +331,73 @@ function prompt { Tuple.Create(ConsoleColor.Blue, ConsoleColor.Magenta), "PSREADLINE>", TokenClassification.Command, "dir")))); } + + [SkippableFact] + public void LengthInBufferCells_SurrogatePair() + { + // πŸ‘‰ is U+1F449, encoded as surrogate pair \uD83D\uDC49 + // A surrogate pair should count as 1 buffer cell. + Assert.Equal(1, PSConsoleReadLine.LengthInBufferCells("\uD83D\uDC49")); + } + + [SkippableFact] + public void LengthInBufferCells_MultipleSurrogatePairs() + { + // "πŸ‘‰βŒ" = two surrogate pairs, should be 2 buffer cells + Assert.Equal(2, PSConsoleReadLine.LengthInBufferCells("πŸ‘‰βŒ")); + } + + [SkippableFact] + public void LengthInBufferCells_SurrogatePairWithText() + { + // "πŸ‘‰ " = surrogate pair + space = 2 buffer cells + Assert.Equal(2, PSConsoleReadLine.LengthInBufferCells("πŸ‘‰ ")); + // "❌ " = surrogate pair + space = 2 buffer cells + Assert.Equal(2, PSConsoleReadLine.LengthInBufferCells("❌ ")); + // "abcπŸ‘‰def" = 3 + 1 + 3 = 7 + Assert.Equal(7, PSConsoleReadLine.LengthInBufferCells("abcπŸ‘‰def")); + } + + [SkippableFact] + public void LengthInBufferCells_SurrogatePairWithEscapeSequence() + { + // ESC[31m (red color) + surrogate pair + ESC[0m (reset) + // Only the surrogate pair should count: 1 buffer cell + Assert.Equal(1, PSConsoleReadLine.LengthInBufferCells("\x1b[31m\uD83D\uDC49\x1b[0m")); + } + + [SkippableFact] + public void LengthInBufferCells_NerdFontGlyph() + { + // U+F015A (nf-md-home) = \uDB80\uDD5A - a supplementary character in the private use area + // "β•°σ°…š " = 1 + 1 + 1 = 3 buffer cells (the issue reported this as 4 before the fix) + Assert.Equal(3, PSConsoleReadLine.LengthInBufferCells("β•°\uDB80\uDD5A ")); + } + + [SkippableFact] + public void LengthInBufferCells_LoneHighSurrogate() + { + // A lone high surrogate without a following low surrogate should still + // be handled without error (falls through to LengthInBufferCells(char)) + var str = "a\uD83Db"; + int expected = 1 + PSConsoleReadLine.LengthInBufferCells('\uD83D') + 1; + Assert.Equal(expected, PSConsoleReadLine.LengthInBufferCells(str)); + } + + [SkippableFact] + public void LengthInBufferCells_StringBuilder_SurrogatePair() + { + var sb = new StringBuilder("πŸ‘‰ "); + // surrogate pair + space = 2 buffer cells + Assert.Equal(2, PSConsoleReadLine.LengthInBufferCells(sb, 0, sb.Length)); + } + + [SkippableFact] + public void LengthInBufferCells_StringBuilder_SurrogatePairSubstring() + { + var sb = new StringBuilder("abcπŸ‘‰def"); + // Just the surrogate pair portion (indices 3 and 4) = 1 buffer cell + Assert.Equal(1, PSConsoleReadLine.LengthInBufferCells(sb, 3, 5)); + } } } From 744a7429bf75d374c8aa5dc48b80ecd9a68e7434 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:08:28 -0700 Subject: [PATCH 3/3] Slight readability cleanup with `char.IsSurrogatePair()` And short circuits faster if at end of the string. --- PSReadLine/Render.Helper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 8fa9ca642..297cdd30f 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -57,7 +57,7 @@ internal static int LengthInBufferCells(string str, int start, int end) for (var i = start; i < end; i++) { var c = str[i]; - if (char.IsHighSurrogate(c) && (i + 1) < end && char.IsLowSurrogate(str[i + 1])) + if ((i + 1) < end && char.IsSurrogatePair(c, str[i + 1])) { sum++; i++; // Skip the low surrogate @@ -83,7 +83,7 @@ internal static int LengthInBufferCells(StringBuilder sb, int start, int end) for (var i = start; i < end; i++) { var c = sb[i]; - if (char.IsHighSurrogate(c) && (i + 1) < end && char.IsLowSurrogate(sb[i + 1])) + if ((i + 1) < end && char.IsSurrogatePair(c, sb[i + 1])) { sum++; i++; // Skip the low surrogate