Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Sharprompt.Tests/EastAsianWidthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ public class EastAsianWidthTests
[InlineData("🍣", 2)]
[InlineData("🍣🍖🥂", 6)]
[InlineData("aあ𩸽🍣", 7)]
[InlineData("\u200D", 0)]
[InlineData("❤️", 2)]
[InlineData("👍🏻", 2)]
[InlineData("1️⃣", 2)]
[InlineData("🇯🇵", 2)]
[InlineData("👩‍💻", 2)]
[InlineData("👨‍👩‍👧‍👦", 2)]
public void GetWidth(string value, int width)
{
Assert.Equal(width, value.GetWidth());
Expand Down
7 changes: 7 additions & 0 deletions Sharprompt.Tests/TextInputBufferTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public void Insert(string value, int length)
[InlineData("𩸽𠈻𠮷", "𩸽𠈻")]
[InlineData("🍣🍖🥂", "🍣🍖")]
[InlineData("aあ𩸽🍣", "aあ𩸽")]
[InlineData("👩‍💻", "")]
[InlineData("a👩‍💻", "a")]
public void Backspace(string value, string substring)
{
var textInputBuffer = new TextInputBuffer();
Expand All @@ -57,6 +59,8 @@ public void Backspace(string value, string substring)
[InlineData("𩸽𠈻𠮷", "𠈻𠮷")]
[InlineData("🍣🍖🥂", "🍖🥂")]
[InlineData("aあ𩸽🍣", "あ𩸽🍣")]
[InlineData("👩‍💻", "")]
[InlineData("👩‍💻a", "a")]
public void Delete(string value, string substring)
{
var textInputBuffer = new TextInputBuffer();
Expand All @@ -81,6 +85,7 @@ public void Delete(string value, string substring)
[InlineData("𩸽𠈻𠮷", "𩸽𠈻", "𠮷")]
[InlineData("🍣🍖🥂", "🍣🍖", "🥂")]
[InlineData("aあ𩸽🍣", "aあ𩸽", "🍣")]
[InlineData("a👩‍💻", "a", "👩‍💻")]
public void MoveBackward(string value, string backward, string forward)
{
var textInputBuffer = new TextInputBuffer();
Expand All @@ -102,6 +107,7 @@ public void MoveBackward(string value, string backward, string forward)
[InlineData("𩸽𠈻𠮷", "𩸽", "𠈻𠮷")]
[InlineData("🍣🍖🥂", "🍣", "🍖🥂")]
[InlineData("aあ𩸽🍣", "a", "あ𩸽🍣")]
[InlineData("👩‍💻a", "👩‍💻", "a")]
public void MoveForward(string value, string backward, string forward)
{
var textInputBuffer = new TextInputBuffer();
Expand Down Expand Up @@ -209,6 +215,7 @@ public void MoveToNextWord(string value, string backward, string forward)
[InlineData("aあ_𩸽🍣", "", "𩸽🍣")]
[InlineData("aあ𩸽_🍣", "", "🍣")]
[InlineData("aあ𩸽🍣_", "", "")]
[InlineData("a👩‍💻_", "", "")]
[InlineData("_ abc def ", "", " abc def ")]
[InlineData(" _abc def ", "", "abc def ")]
[InlineData(" a_bc def ", " ", "bc def ")]
Expand Down
77 changes: 74 additions & 3 deletions Sharprompt/Internal/EastAsianWidth.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
namespace Sharprompt.Internal;
using System.Globalization;
using System.Text;

namespace Sharprompt.Internal;

internal static class EastAsianWidth
{
private const char ZeroWidthJoiner = '\u200D';

public static int GetWidth(this string value)
{
var width = 0;
var textElementEnumerator = StringInfo.GetTextElementEnumerator(value);

while (textElementEnumerator.MoveNext())
{
width += GetTextElementWidth(textElementEnumerator.GetTextElement());
}

return width;
}

private static int GetTextElementWidth(string value)
{
var width = 0;
var hasVisibleCodePoint = false;
var hasEmojiSequence = false;
var regionalIndicatorCount = 0;

for (var i = 0; i < value.Length; i++)
{
Expand All @@ -21,13 +42,63 @@ public static int GetWidth(this string value)
codePoint = value[i];
}

width += GetWidth(codePoint);
if (IsEmojiSequenceCodePoint(codePoint))
{
hasEmojiSequence = true;
}

if (IsRegionalIndicator(codePoint))
{
regionalIndicatorCount++;
}

var codePointWidth = GetWidth(codePoint);

if (codePointWidth > 0)
{
hasVisibleCodePoint = true;
width += codePointWidth;
}
}

if (!hasVisibleCodePoint)
{
return 0;
}

if (hasEmojiSequence || regionalIndicatorCount == 2)
{
return 2;
}

return width;
}

private static int GetWidth(uint codePoint) => IsFullWidth(codePoint) ? 2 : 1;
private static int GetWidth(uint codePoint) => IsZeroWidth(codePoint) ? 0 : IsFullWidth(codePoint) ? 2 : 1;

private static bool IsZeroWidth(uint codePoint)
{
if (!Rune.TryCreate((int)codePoint, out var rune))
{
return false;
}

return Rune.GetUnicodeCategory(rune) is UnicodeCategory.Control or UnicodeCategory.Format or UnicodeCategory.NonSpacingMark or UnicodeCategory.EnclosingMark;
}

private static bool IsEmojiSequenceCodePoint(uint codePoint) =>
codePoint == ZeroWidthJoiner ||
IsVariationSelector(codePoint) ||
IsEmojiModifier(codePoint) ||
codePoint == 0x20E3;

private static bool IsVariationSelector(uint codePoint) =>
(codePoint >= 0xFE00 && codePoint <= 0xFE0F) ||
(codePoint >= 0xE0100 && codePoint <= 0xE01EF);

private static bool IsEmojiModifier(uint codePoint) => codePoint is >= 0x1F3FB and <= 0x1F3FF;

private static bool IsRegionalIndicator(uint codePoint) => codePoint is >= 0x1F1E6 and <= 0x1F1FF;

private static bool IsFullWidth(uint codePoint)
{
Expand Down
Loading
Loading