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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,5 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
coverage.opencover.xml

history-file
11 changes: 6 additions & 5 deletions src/PrettyPrompt/Documents/WordWrapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ public static WordWrappedText WrapEditableCharacters(ReadOnlyStringBuilder input
bool isCursorPastCharacter = caret > textIndex;

Debug.Assert(character != '\t', "tabs should be replaced by spaces");
// Zero-width scalars (combining marks, zero-width joiners, variation selectors, etc.) are
// legitimate input - e.g. when pasting an emoji such as 🤦🏼‍♂️. They contribute no display
// width (so they don't grow the line or trigger wrapping), but they ARE part of the
// string/line content, so they must still advance textIndex (the string index) and the
// caret column just like any other character. See issue #270.
// Zero-width scalars (combining marks, zero-width joiners, etc.) are legitimate input - e.g.
// when pasting an emoji such as 🤦🏼‍♂️. They contribute no display width (so they don't grow the
// line or trigger wrapping), but they ARE part of the string/line content, so they must still
// advance textIndex (the string index) and the caret column just like any other character.
// (The emoji variation selector U+FE0F is the exception: GetWidth gives it one column, because
// it promotes its base to a 2-column emoji - see UnicodeWidth.) See issue #270.
int unicodeWidth = UnicodeWidth.GetWidth(character);
currentLineLength += unicodeWidth;
textIndex++;
Expand Down
7 changes: 7 additions & 0 deletions src/PrettyPrompt/Rendering/UnicodeWidth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public static int GetWidth(char character)
{
if (character == '\n') return 1; // PrettyPrompt: treat newline as occupying a single column.
if (char.IsSurrogate(character)) return 1; // half of a surrogate pair; the pair sums to the scalar's width.
// U+FE0F (emoji variation selector) carries the column its base gains from emoji presentation - e.g.
// ⚠ (1 col) + VS16 = a 2-col emoji - keeping this per-char sum equal to GetGraphemeClusterWidth.
if (character == (char)0xFE0F) return 1;
return Clamp(UnicodeCalculator.GetWidth(character));
}

Expand Down Expand Up @@ -70,6 +73,10 @@ public static int GetGraphemeClusterWidth(ReadOnlySpan<char> cluster)
return 1; // ill-formed (e.g. a lone surrogate); be defensive and reserve a single column.
}
if (baseRune.Value == '\n') return 1;
// U+FE0F (emoji variation selector) forces emoji presentation, which is 2 columns even for a base
// that defaults to 1 - e.g. ⚠ (U+26A0) is 1 column but ⚠️ is a 2-column emoji; wcwidth misses this.
// The length check skips a lone, base-less selector (no width of its own).
if (cluster.Length > 1 && cluster.Contains((char)0xFE0F)) return 2;
return Clamp(UnicodeCalculator.GetWidth(baseRune));
}

Expand Down
10 changes: 10 additions & 0 deletions tests/PrettyPrompt.Tests/UnicodeWidthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ public class UnicodeWidthTests

// narrow supplementary glyph that the old code mis-sized (issue called this out): a playing card is 1
[InlineData("\U0001F0A1", 1)] // 🂡 playing card ace of spades

// U+FE0F (emoji variation selector) promotes a default-text base into a two-column emoji. The bare base
// is one column, but base + VS16 renders as a 2-column emoji in terminals, so the caret/cursor must size
// it as two columns to stay aligned past it. See https://github.com/waf/PrettyPrompt/issues/270.
[InlineData("⚠", 1)] // ⚠ warning sign, text presentation = 1 column
[InlineData("⚠️", 2)] // ⚠️ warning sign + VS16, emoji presentation = 2 columns
[InlineData("abc⚠️def", 8)] // surrounded: abc (3) + ⚠️ (2) + def (3)
[InlineData("ℹ️", 2)] // ℹ️ information source + VS16
public void GetWidth_ReturnsExpectedDisplayWidth(string text, int expectedWidth)
{
Assert.Equal(expectedWidth, UnicodeWidth.GetWidth(text));
Expand All @@ -50,6 +58,8 @@ public void GetWidth_ReturnsExpectedDisplayWidth(string text, int expectedWidth)
[InlineData("\u79B0\U000E0100", 2)] // 禰󠄀
[InlineData("a", 1)] // narrow
[InlineData("\u4E66", 2)] // 书 wide
[InlineData("⚠", 1)] // ⚠ warning sign on its own = text presentation, 1 column
[InlineData("⚠️", 2)] // ⚠️ warning sign + VS16 = emoji presentation, 2 columns
public void GetGraphemeClusterWidth_IsCappedAtTwo(string cluster, int expectedWidth)
{
Assert.Equal(expectedWidth, UnicodeWidth.GetGraphemeClusterWidth(cluster));
Expand Down
18 changes: 18 additions & 0 deletions tests/PrettyPrompt.Tests/WordWrappingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ public void WrapEditableCharacters_DoubleWidthCharactersWithWrappingInMiddleOfCh

}

[Fact]
public void WrapEditableCharacters_EmojiVariationSelectorAtBoundary_OccupiesTwoColumns()
{
// ⚠️ (U+26A0 U+FE0F) is a 2-column emoji. With width 6, "abcd" (4 cols) + ⚠️ (2 cols) must exactly
// fill the first line - the selector is NOT zero-width here - so "efgh" wraps to the next line.
var text = "abcd⚠️efgh";
var wrapped = WordWrapping.WrapEditableCharacters(new StringBuilder(text), caret: 0, width: 6);

Assert.Equal(
new[]
{
new WrappedLine(0, "abcd⚠️"),
new WrappedLine(6, "efgh"),
},
wrapped.WrappedLines
);
}

[Fact]
public void WrapWords_GivenLongText_WrapsWords()
{
Expand Down
Loading