diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 8fc56bea..297cdd30 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 ((i + 1) < end && char.IsSurrogatePair(c, 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 ((i + 1) < end && char.IsSurrogatePair(c, 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; diff --git a/test/RenderTest.cs b/test/RenderTest.cs index cc72f367..74cd5271 100644 --- a/test/RenderTest.cs +++ b/test/RenderTest.cs @@ -340,5 +340,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)); + } } }