Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Management.Automation.Internal;
using System.Text;


Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Unnecessary blank line added. This extra whitespace doesn't serve any purpose and should be removed.

Suggested change

Copilot uses AI. Check for mistakes.
// interfaces for host interaction

namespace Microsoft.PowerShell.Commands.Internal.Format
Expand Down Expand Up @@ -50,14 +51,49 @@ internal virtual int Length(string str, int offset)
str = valueStrDec.ToString(OutputRendering.PlainText);
}

// Use StringInfo to enumerate grapheme clusters
int length = 0;
for (; offset < str.Length; offset++)
System.Globalization.StringInfo si = new System.Globalization.StringInfo(str);
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable si is declared but never used. It can be removed since only ParseCombiningCharacters() is needed here.

Suggested change
System.Globalization.StringInfo si = new System.Globalization.StringInfo(str);

Copilot uses AI. Check for mistakes.
int[] textElementIndexes = System.Globalization.StringInfo.ParseCombiningCharacters(str);
for (int i = 0; i < textElementIndexes.Length; i++)
{
length += CharLengthInBufferCells(str[offset]);
if (i < offset) continue;
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offset parameter semantics have changed in an incompatible way. Previously, offset was a character index in the string. Now it's being treated as an index into the textElementIndexes array (line 60: if (i < offset) continue). This is a breaking change that could cause incorrect behavior when callers pass a character offset expecting the old behavior.

Consider either:

  1. Maintaining backward compatibility by converting the character offset to a text element index, or
  2. Documenting this breaking change if intentional

Copilot uses AI. Check for mistakes.
int start = textElementIndexes[i];
int graphemeLength = (i + 1 < textElementIndexes.Length)
? textElementIndexes[i + 1] - textElementIndexes[i]
: str.Length - textElementIndexes[i];
string grapheme = str.Substring(start, graphemeLength);
length += GraphemeLengthInBufferCells(grapheme);
}

return length;
Comment on lines +54 to 68
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance concern: Using StringInfo.ParseCombiningCharacters() and Substring() for every call to Length() can be significantly slower than the previous character-by-character iteration, especially for long strings. ParseCombiningCharacters() allocates an array and performs multiple string scans.

For most strings (which don't contain complex grapheme clusters), this is unnecessary overhead. Consider adding a fast path that only uses the grapheme cluster approach when necessary:

// Fast path: check if string likely contains complex graphemes
bool needsGraphemeProcessing = str.Any(c => char.IsSurrogate(c) || c >= 0x0300);
if (!needsGraphemeProcessing) {
    // Use simpler character-by-character processing
} else {
    // Use grapheme cluster processing
}

Copilot uses AI. Check for mistakes.
}
/// <summary>
/// Calculate the buffer cell length of a grapheme cluster (text element).
/// </summary>
/// <param name="grapheme">A string representing a single grapheme cluster.</param>
/// <returns>Number of buffer cells the grapheme needs to take.</returns>
protected virtual int GraphemeLengthInBufferCells(string grapheme)
{
if (string.IsNullOrEmpty(grapheme))
return 0;

// Check if the grapheme is an emoji (basic Unicode ranges)
int codePoint = char.ConvertToUtf32(grapheme, 0);
// Emoji ranges: Emoticons, Misc Symbols, Dingbats, Transport, etc.
if ((codePoint >= 0x1F600 && codePoint <= 0x1F64F) || // Emoticons
(codePoint >= 0x1F300 && codePoint <= 0x1F5FF) || // Misc Symbols & Pictographs
(codePoint >= 0x1F680 && codePoint <= 0x1F6FF) || // Transport & Map
(codePoint >= 0x2600 && codePoint <= 0x26FF) || // Misc symbols
(codePoint >= 0x2700 && codePoint <= 0x27BF) || // Dingbats
(codePoint >= 0x1F900 && codePoint <= 0x1F9FF) || // Supplemental Symbols & Pictographs
(codePoint >= 0x1FA70 && codePoint <= 0x1FAFF) || // Symbols & Pictographs Extended-A
(codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF)) // Regional Indicator Symbols
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing several important emoji ranges that should be treated as wide (2 cells):

  • 0x1F000-0x1F02F (Mahjong Tiles, Domino Tiles)
  • 0x1F0A0-0x1F0FF (Playing Cards)
  • 0x1F100-0x1F5FF (Enclosed characters)
  • 0x1FA00-0x1FA6F (Chess Symbols, etc.)
  • 0x2B50-0x2B55 (Stars and symbols)
  • 0x2300-0x23FF (Miscellaneous Technical)

These ranges are covered in CodePointLengthInBufferCells() but should also be included here for consistency.

Suggested change
(codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF)) // Regional Indicator Symbols
(codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF) || // Regional Indicator Symbols
(codePoint >= 0x1F000 && codePoint <= 0x1F02F) || // Mahjong Tiles, Domino Tiles
(codePoint >= 0x1F0A0 && codePoint <= 0x1F0FF) || // Playing Cards
(codePoint >= 0x1F100 && codePoint <= 0x1F5FF) || // Enclosed characters
(codePoint >= 0x1FA00 && codePoint <= 0x1FA6F) || // Chess Symbols, etc.
(codePoint >= 0x2B50 && codePoint <= 0x2B55) || // Stars and symbols
(codePoint >= 0x2300 && codePoint <= 0x23FF)) // Miscellaneous Technical

Copilot uses AI. Check for mistakes.
{
return 2;
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GraphemeLengthInBufferCells() method doesn't handle wide CJK characters properly. It only checks for emoji ranges but ignores the existing CJK width logic in CharLengthInBufferCells(). A grapheme containing CJK characters should still be calculated as 2 cells wide, but this implementation defaults to returning 1 (line 95).

Consider falling back to CharLengthInBufferCells() for BMP characters that aren't emojis, similar to how CodePointLengthInBufferCells() does on lines 182-184.

Suggested change
}
}
// For BMP characters, use CharLengthInBufferCells to handle CJK/wide logic
if (codePoint <= 0xFFFF)
return CharLengthInBufferCells((char)codePoint);

Copilot uses AI. Check for mistakes.
// Default width for other graphemes
return 1;
}
Comment on lines +70 to +96
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: This method has excessive indentation (appears to be indented as if it's inside another block). It should align with other methods in the class.

Suggested change
/// <summary>
/// Calculate the buffer cell length of a grapheme cluster (text element).
/// </summary>
/// <param name="grapheme">A string representing a single grapheme cluster.</param>
/// <returns>Number of buffer cells the grapheme needs to take.</returns>
protected virtual int GraphemeLengthInBufferCells(string grapheme)
{
if (string.IsNullOrEmpty(grapheme))
return 0;
// Check if the grapheme is an emoji (basic Unicode ranges)
int codePoint = char.ConvertToUtf32(grapheme, 0);
// Emoji ranges: Emoticons, Misc Symbols, Dingbats, Transport, etc.
if ((codePoint >= 0x1F600 && codePoint <= 0x1F64F) || // Emoticons
(codePoint >= 0x1F300 && codePoint <= 0x1F5FF) || // Misc Symbols & Pictographs
(codePoint >= 0x1F680 && codePoint <= 0x1F6FF) || // Transport & Map
(codePoint >= 0x2600 && codePoint <= 0x26FF) || // Misc symbols
(codePoint >= 0x2700 && codePoint <= 0x27BF) || // Dingbats
(codePoint >= 0x1F900 && codePoint <= 0x1F9FF) || // Supplemental Symbols & Pictographs
(codePoint >= 0x1FA70 && codePoint <= 0x1FAFF) || // Symbols & Pictographs Extended-A
(codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF)) // Regional Indicator Symbols
{
return 2;
}
// Default width for other graphemes
return 1;
}
/// <summary>
/// Calculate the buffer cell length of a grapheme cluster (text element).
/// </summary>
/// <param name="grapheme">A string representing a single grapheme cluster.</param>
/// <returns>Number of buffer cells the grapheme needs to take.</returns>
protected virtual int GraphemeLengthInBufferCells(string grapheme)
{
if (string.IsNullOrEmpty(grapheme))
return 0;
// Check if the grapheme is an emoji (basic Unicode ranges)
int codePoint = char.ConvertToUtf32(grapheme, 0);
// Emoji ranges: Emoticons, Misc Symbols, Dingbats, Transport, etc.
if ((codePoint >= 0x1F600 && codePoint <= 0x1F64F) || // Emoticons
(codePoint >= 0x1F300 && codePoint <= 0x1F5FF) || // Misc Symbols & Pictographs
(codePoint >= 0x1F680 && codePoint <= 0x1F6FF) || // Transport & Map
(codePoint >= 0x2600 && codePoint <= 0x26FF) || // Misc symbols
(codePoint >= 0x2700 && codePoint <= 0x27BF) || // Dingbats
(codePoint >= 0x1F900 && codePoint <= 0x1F9FF) || // Supplemental Symbols & Pictographs
(codePoint >= 0x1FA70 && codePoint <= 0x1FAFF) || // Symbols & Pictographs Extended-A
(codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF)) // Regional Indicator Symbols
{
return 2;
}
// Default width for other graphemes
return 1;
}

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +96
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GraphemeLengthInBufferCells() method only checks the first code point of a grapheme cluster (line 81), but grapheme clusters can contain multiple code points (e.g., base character + combining marks, or emoji with skin tone modifiers). This will give incorrect results for complex graphemes.

For example, "👨‍👩‍👧" (family emoji) is a single grapheme but consists of multiple code points joined with zero-width joiners. The current implementation would only check the first code point.

Consider using a more comprehensive approach that accounts for the full grapheme structure, or leveraging existing Unicode width calculation libraries.

Suggested change
// Check if the grapheme is an emoji (basic Unicode ranges)
int codePoint = char.ConvertToUtf32(grapheme, 0);
// Emoji ranges: Emoticons, Misc Symbols, Dingbats, Transport, etc.
if ((codePoint >= 0x1F600 && codePoint <= 0x1F64F) || // Emoticons
(codePoint >= 0x1F300 && codePoint <= 0x1F5FF) || // Misc Symbols & Pictographs
(codePoint >= 0x1F680 && codePoint <= 0x1F6FF) || // Transport & Map
(codePoint >= 0x2600 && codePoint <= 0x26FF) || // Misc symbols
(codePoint >= 0x2700 && codePoint <= 0x27BF) || // Dingbats
(codePoint >= 0x1F900 && codePoint <= 0x1F9FF) || // Supplemental Symbols & Pictographs
(codePoint >= 0x1FA70 && codePoint <= 0x1FAFF) || // Symbols & Pictographs Extended-A
(codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF)) // Regional Indicator Symbols
{
return 2;
}
// Default width for other graphemes
return 1;
}
int width = 0;
for (int i = 0; i < grapheme.Length;)
{
int codePoint = char.ConvertToUtf32(grapheme, i);
// Skip zero-width joiner (U+200D) and combining marks
if (codePoint == 0x200D || CharUnicodeInfo.GetUnicodeCategory(grapheme, i) == UnicodeCategory.NonSpacingMark)
{
i += char.IsSurrogatePair(grapheme, i) ? 2 : 1;
continue;
}
width += CharLengthInBufferCells(char.ConvertFromUtf32(codePoint), codePoint);
i += char.IsSurrogatePair(grapheme, i) ? 2 : 1;
}
return width;
}

Copilot uses AI. Check for mistakes.

/// <summary>
/// Calculate the buffer cell length of the given character.
Expand Down Expand Up @@ -120,8 +156,50 @@ internal int TruncateHead(string str, int displayCells)

#region Helpers

protected static int CodePointLengthInBufferCells(int codePoint)
{
// Emoji and symbol ranges (most emojis are wide/2-cell)
// Based on Unicode standard emoji ranges
if ((codePoint >= 0x1F300 && codePoint <= 0x1F9FF) || // Miscellaneous Symbols and Pictographs, Emoticons, etc.
(codePoint >= 0x1F000 && codePoint <= 0x1F02F) || // Mahjong Tiles, Domino Tiles
(codePoint >= 0x1F0A0 && codePoint <= 0x1F0FF) || // Playing Cards
(codePoint >= 0x1F100 && codePoint <= 0x1F64F) || // Enclosed Alphanumeric Supplement, Emoticons
(codePoint >= 0x1F680 && codePoint <= 0x1F6FF) || // Transport and Map Symbols
(codePoint >= 0x1F900 && codePoint <= 0x1F9FF) || // Supplemental Symbols and Pictographs
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overlapping ranges detected: The range 0x1F300-0x1F9FF on line 163 already includes 0x1F900-0x1F9FF (line 168). The duplicate check on line 168 is redundant and should be removed.

Suggested change
(codePoint >= 0x1F900 && codePoint <= 0x1F9FF) || // Supplemental Symbols and Pictographs

Copilot uses AI. Check for mistakes.
(codePoint >= 0x1FA00 && codePoint <= 0x1FA6F) || // Chess Symbols, Symbols and Pictographs Extended-A
(codePoint >= 0x1FA70 && codePoint <= 0x1FAFF) || // Symbols and Pictographs Extended-A
(codePoint >= 0x2600 && codePoint <= 0x26FF) || // Miscellaneous Symbols (includes some emojis)
(codePoint >= 0x2700 && codePoint <= 0x27BF) || // Dingbats
(codePoint >= 0x1F170 && codePoint <= 0x1F251) || // Enclosed Alphanumeric Supplement
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overlapping ranges detected: The range 0x1F100-0x1F64F on line 166 already includes 0x1F170-0x1F251 (line 173). The duplicate check on line 173 is redundant and should be removed.

Suggested change
(codePoint >= 0x1F170 && codePoint <= 0x1F251) || // Enclosed Alphanumeric Supplement

Copilot uses AI. Check for mistakes.
(codePoint >= 0x1F3FB && codePoint <= 0x1F3FF) || // Emoji skin tone modifiers
(codePoint >= 0x20000 && codePoint <= 0x2FFFD) || // CJK Extension B-F
(codePoint >= 0x30000 && codePoint <= 0x3FFFD)) // CJK Extension G and beyond
{
return 2;
}

// For BMP characters, use the existing logic
if (codePoint <= 0xFFFF)
{
return CharLengthInBufferCells((char)codePoint);
}

// Default for other supplementary characters
return 2;
}

protected static int CharLengthInBufferCells(char c)
{
// Check for BMP emojis that are 2 cells wide
// These are common emoji characters that don't require surrogate pairs
if ((c >= 0x2600 && c <= 0x26FF) || // Miscellaneous Symbols (many emojis)
(c >= 0x2700 && c <= 0x27BF) || // Dingbats
(c >= 0x2300 && c <= 0x23FF) || // Miscellaneous Technical
(c >= 0x2B50 && c <= 0x2B55)) // Stars and other symbols
{
return 2;
}

// The following is based on http://www.cl.cam.ac.uk/~mgk25/c/wcwidth.c
// which is derived from https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt
bool isWide = c >= 0x1100 &&
Expand All @@ -136,10 +214,6 @@ protected static int CharLengthInBufferCells(char c)
((uint)(c - 0xff00) <= (0xff60 - 0xff00)) || /* Fullwidth Forms */
((uint)(c - 0xffe0) <= (0xffe6 - 0xffe0)));

// We can ignore these ranges because .Net strings use surrogate pairs
// for this range and we do not handle surrogate pairs.
// (c >= 0x20000 && c <= 0x2fffd) ||
// (c >= 0x30000 && c <= 0x3fffd)
return 1 + (isWide ? 1 : 0);
}

Expand Down Expand Up @@ -167,8 +241,29 @@ protected int GetFitLength(string str, int offset, int displayCells, bool startF
break;
}

// compute the cell number for the current character
currCharDisplayLen = this.Length(str[k]);
// compute the cell number for the current character or surrogate pair
if (startFromHead && char.IsHighSurrogate(str[k]) && k + 1 <= kFinal && char.IsLowSurrogate(str[k + 1]))
{
// This is a surrogate pair when going forward
int codePoint = char.ConvertToUtf32(str[k], str[k + 1]);
currCharDisplayLen = CodePointLengthInBufferCells(codePoint);
}
else if (!startFromHead && char.IsLowSurrogate(str[k]) && k - 1 >= kFinal && char.IsHighSurrogate(str[k - 1]))
{
// This is a surrogate pair when going backward - skip it since we'll process with the high surrogate
k = startFromHead ? (k + 1) : (k - 1);
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condition always evaluates to 'false'.

Suggested change
k = startFromHead ? (k + 1) : (k - 1);
k = k - 1;

Copilot uses AI. Check for mistakes.
continue;
}
Comment on lines +251 to +256
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Potential issue with backward traversal: When going backward and encountering a low surrogate at line 251, the code skips it (line 254) but then at line 299, k is decremented again. This could cause the algorithm to skip over the high surrogate entirely without processing it.

Example: If k is at a low surrogate and startFromHead=false:

  1. Line 251 detects low surrogate
  2. Line 254: k = k - 1 (now at high surrogate)
  3. continue jumps back to loop start
  4. Line 239 check passes
  5. Line 257 detects high surrogate and processes it correctly
  6. Line 299: k = k - 1 (moves backward)

Actually, on second review, this appears correct since the continue statement jumps back to the start of the loop. However, the logic is confusing and error-prone.

Copilot uses AI. Check for mistakes.
else if (!startFromHead && char.IsHighSurrogate(str[k]) && k + 1 < str.Length && char.IsLowSurrogate(str[k + 1]))
{
// We're at the high surrogate going backward - process the pair
int codePoint = char.ConvertToUtf32(str[k], str[k + 1]);
currCharDisplayLen = CodePointLengthInBufferCells(codePoint);
}
else
{
currCharDisplayLen = CharLengthInBufferCells(str[k]);
}

if (filledDisplayCellsCount + currCharDisplayLen > displayCells)
{
Expand All @@ -178,7 +273,21 @@ protected int GetFitLength(string str, int offset, int displayCells, bool startF

// keep adding, we fit
filledDisplayCellsCount += currCharDisplayLen;
charactersAdded++;

// Count the number of char units (1 for BMP, 2 for surrogate pairs)
if (startFromHead && char.IsHighSurrogate(str[k]) && k + 1 <= kFinal && char.IsLowSurrogate(str[k + 1]))
{
charactersAdded += 2; // surrogate pair
k++; // skip the low surrogate
}
else if (!startFromHead && char.IsHighSurrogate(str[k]) && k + 1 < str.Length && char.IsLowSurrogate(str[k + 1]))
{
charactersAdded += 2; // surrogate pair
}
else
{
charactersAdded++;
}

// check if we fit exactly
if (filledDisplayCellsCount == displayCells)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<!-- the following package(s) are from the powershell org -->
<PackageReference Include="Microsoft.Management.Infrastructure" Version="3.0.0" />
<PackageReference Include="Microsoft.PowerShell.Native" Version="700.0.0-preview.2" />
<!-- Removed Wcwidth.Net package reference -->
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment about removing Wcwidth.Net package reference doesn't add useful information and creates an orphaned comment line. The comment should either be removed entirely or clarified to explain why the package was removed (e.g., "Native emoji width calculation implemented to replace Wcwidth.Net dependency").

Suggested change
<!-- Removed Wcwidth.Net package reference -->
<!-- Removed Wcwidth.Net package reference: native emoji width calculation implemented to replace Wcwidth.Net dependency -->

Copilot uses AI. Check for mistakes.
<!-- Signing APIs -->
<PackageReference Include="Microsoft.Security.Extensions" Version="1.4.0" />
</ItemGroup>
Expand Down
110 changes: 110 additions & 0 deletions test_emoji.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;

class EmojiTest
{
static int CodePointLengthInBufferCells(int codePoint)
{
// Emoji and symbol ranges (most emojis are wide/2-cell)
if ((codePoint >= 0x1F300 && codePoint <= 0x1F9FF) ||
(codePoint >= 0x1F000 && codePoint <= 0x1F02F) ||
(codePoint >= 0x1F0A0 && codePoint <= 0x1F0FF) ||
(codePoint >= 0x1F100 && codePoint <= 0x1F64F) ||
(codePoint >= 0x1F680 && codePoint <= 0x1F6FF) ||
(codePoint >= 0x1F900 && codePoint <= 0x1F9FF) ||
(codePoint >= 0x1FA00 && codePoint <= 0x1FA6F) ||
(codePoint >= 0x1FA70 && codePoint <= 0x1FAFF) ||
(codePoint >= 0x2600 && codePoint <= 0x26FF) ||
(codePoint >= 0x2700 && codePoint <= 0x27BF) ||
(codePoint >= 0x1F170 && codePoint <= 0x1F251) ||
(codePoint >= 0x1F3FB && codePoint <= 0x1F3FF) ||
(codePoint >= 0x20000 && codePoint <= 0x2FFFD) ||
(codePoint >= 0x30000 && codePoint <= 0x3FFFD))
{
return 2;
}

if (codePoint <= 0xFFFF)
{
return CharLengthInBufferCells((char)codePoint);
}

return 2;
}

static int CharLengthInBufferCells(char c)
{
// Check for BMP emojis that are 2 cells wide
if ((c >= 0x2600 && c <= 0x26FF) ||
(c >= 0x2700 && c <= 0x27BF) ||
(c >= 0x2300 && c <= 0x23FF) ||
(c >= 0x2B50 && c <= 0x2B55))
{
return 2;
}

bool isWide = c >= 0x1100 &&
(c <= 0x115f ||
c == 0x2329 || c == 0x232a ||
((uint)(c - 0x2e80) <= (0xa4cf - 0x2e80) &&
c != 0x303f) ||
((uint)(c - 0xac00) <= (0xd7a3 - 0xac00)) ||
((uint)(c - 0xf900) <= (0xfaff - 0xf900)) ||
((uint)(c - 0xfe10) <= (0xfe19 - 0xfe10)) ||
((uint)(c - 0xfe30) <= (0xfe6f - 0xfe30)) ||
((uint)(c - 0xff00) <= (0xff60 - 0xff00)) ||
((uint)(c - 0xffe0) <= (0xffe6 - 0xffe0)));

return 1 + (isWide ? 1 : 0);
}

static int CalculateLength(string str)
{
int length = 0;
for (int i = 0; i < str.Length; i++)
{
char c = str[i];

if (char.IsHighSurrogate(c) && i + 1 < str.Length && char.IsLowSurrogate(str[i + 1]))
{
int codePoint = char.ConvertToUtf32(c, str[i + 1]);
length += CodePointLengthInBufferCells(codePoint);
i++; // Skip the low surrogate
}
else
{
length += CharLengthInBufferCells(c);
}
}
return length;
}

static void Main()
{
Console.WriteLine("Testing emoji width calculation:");
Console.WriteLine();

string[] testStrings = {
"✅", "⛔", "🛶", "🌵",
"Yes", "No", "Canoe", "Cactus"
};

foreach (var str in testStrings)
{
int width = CalculateLength(str);
Console.WriteLine($"String: '{str}' | Calculated Width: {width} | Actual Length: {str.Length}");
}

Console.WriteLine();
Console.WriteLine("Full table test:");
Console.WriteLine();

string[] row1 = { "✅", "Yes", "🛶", "Canoe" };
string[] row2 = { "⛔", "No", "🌵", "Cactus" };

Console.WriteLine("Column widths with emoji-aware calculation:");
for (int i = 0; i < row1.Length; i++)
{
Console.WriteLine($"Column {i}: '{row1[i]}' = {CalculateLength(row1[i])} cells, '{row2[i]}' = {CalculateLength(row2[i])} cells");
}
}
}
Comment on lines +1 to +110
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file appears to be placed in the repository root directory, which is not the standard location for test files. Test files should typically be placed in the test/ directory with an appropriate subdirectory structure.

Consider moving this file to an appropriate location in the test/ directory, or removing it if it's only meant as a temporary validation script.

Copilot uses AI. Check for mistakes.