Skip to content

support variation selectors, ZWJ sequences and surrogate pairs in length calculation#2082

Merged
patriksvensson merged 3 commits intospectreconsole:mainfrom
fabsenet:1847-unicode-widechar-table
Apr 16, 2026
Merged

support variation selectors, ZWJ sequences and surrogate pairs in length calculation#2082
patriksvensson merged 3 commits intospectreconsole:mainfrom
fabsenet:1847-unicode-widechar-table

Conversation

@fabsenet
Copy link
Copy Markdown
Contributor

@fabsenet fabsenet commented Apr 13, 2026

Fixes #1847

  • I have read the Contribution Guidelines
  • I have checked that there isn't already another pull request that solves the above issue
  • All newly added code is adequately covered by tests
  • All existing tests are still running without errors

AI

I am working with unicode, windows-1252 and codepages as part of my day job for quite some time. I used Claude Code here as a tool to write faster. I checked every little change myself.

Changes

src/Spectre.Console/Internal/Cell.cs

  • GetCellLength(string): NET6+ → delegates to UnicodeCalculator.GetWidth(string) which already handles grapheme clusters correctly via EnumerateRunes(); netstandard2.0 → iterates with StringInfo.GetTextElementEnumerator
  • GetCellLength(ReadOnlySpan<char>): now delegates to the string path instead of iterating char-by-char
  • New private GetClusterCellLength(string): FE0F → 2, ZWJ → 2, surrogate pair → code point width lookup

src/Spectre.Console/Rendering/Segment.cs

  • Split(): char-foreach replaced with StringInfo enumerator — prevents splits in the middle of a grapheme cluster
  • Truncate(): same change
  • SplitSegment(): same change

src/Spectre.Console.Tests/Unit/CellTests.cs (new)

  • 8 parameterized tests: ASCII, CJK, ♥, ❤️, ZWJ family sequence, ❤️‍🔥, flag 🇩🇪, empty string

src/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs

  • New snapshot test Should_Render_Table_With_Wide_Emoji_Correctly

Please upvote 👍 this pull request if you are interested in it.

@fabsenet
Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

@fabsenet
Copy link
Copy Markdown
Contributor Author

you have to look at the expected output in the terminal as also vs code or github tend to render it incorrectly!

grafik

@patriksvensson
Copy link
Copy Markdown
Contributor

Sorry, but I have a bit difficulty understanding these changes. I appreciate the PR but all logic for Unicode calculation should go into the wcwidth library.

Why use UnicodeCalculator on net6 and above and a complete different approach otherwise?

@fabsenet
Copy link
Copy Markdown
Contributor Author

the problem is the length counting in char by char.
the wide heart basically has two unicode chars "heart + wide" but the original logic was counting them one by one and "wide" on its own has a width of zero. So the original logic has 1+0=1. but the actual printed char takes the space of 2 normal characters and thats why the table is drawn wrong.

the easy fix would have been to fix the one-by-one counting to detect the "wide" and change the result but it would have been a single fix only. this approach now fixes more cases.

@patriksvensson
Copy link
Copy Markdown
Contributor

The string overload in UnicodeCalculator does this already afaik.

@patriksvensson
Copy link
Copy Markdown
Contributor

I appreciate it, but I would rather get the calculation correct in wcwidth to be honest since it's used by other projects as well (the reason for it existing).

I'll take a look at this PR as soon as I can.

@fabsenet fabsenet force-pushed the 1847-unicode-widechar-table branch from 8ae634d to fbeae03 Compare April 14, 2026 00:55
public static int GetCellLength(ReadOnlySpan<char> text)
public static int GetCellLength(string text)
{
#if !NETSTANDARD2_0
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this obviosly is now not fixed for .net standard 2.0. UnicodeCalculator.GetWidth(string) is not available for .net standard?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You are completely correct. I look forward to drop netstandard2.0 at some point (around the 2.0 release of Spectre.Console). For now, I'm OK with having two different implementations (and potentially erroneous rendering on .NET Framework). What do you think?

@fabsenet
Copy link
Copy Markdown
Contributor Author

i reworked it to use the UnicodeCalculator.getstring more, this makes it a bit easier now!

@patriksvensson
Copy link
Copy Markdown
Contributor

Ok, I've looked through it, and I think I understand the changes now. Will take it for a spin after I've merged another emoji-related bug tonight.

Copy link
Copy Markdown
Contributor

@patriksvensson patriksvensson left a comment

Choose a reason for hiding this comment

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

I think this looks good. No tests are failing and it solves the issue. Some minor nit-pick thing that needs to be fixed though.

Comment thread src/Spectre.Console/Rendering/Segment.cs Outdated
@fabsenet
Copy link
Copy Markdown
Contributor Author

Hi i am not sure what to do right now. i am fine with the change of extra brackets but i do not see an accept button anywhere? do i need to put this change in my branch? i see no simple accept button anywhere?

@patriksvensson
Copy link
Copy Markdown
Contributor

I already took care of it 🙂

@patriksvensson patriksvensson merged commit 0525e4c into spectreconsole:main Apr 16, 2026
3 checks passed
@patriksvensson
Copy link
Copy Markdown
Contributor

Merged! Thank you for your contribution. Much appreciated! 👍

This was referenced Apr 22, 2026
@tmakluf
Copy link
Copy Markdown

tmakluf commented Apr 24, 2026

I'm guessing this update made live rendering much slower.
It was very noticeable after upgrading from 55.1

55.1:
55 1

55.2 and latest:
55 2

@fabsenet
Copy link
Copy Markdown
Contributor Author

i watched the small videos now 10+ times and i do not understand what you want to show here. I am fine with trusting you, that it feels slower, but the animation does not transport it.

@patriksvensson
Copy link
Copy Markdown
Contributor

@tmakluf Can you open a new issue? Thanks!

microsoft-github-policy-service Bot pushed a commit to Azure/bicep that referenced this pull request Apr 24, 2026
Updated
[Spectre.Console](https://github.com/spectreconsole/spectre.console)
from 0.54.0 to 0.55.2.

<details>
<summary>Release notes</summary>

_Sourced from [Spectre.Console's
releases](https://github.com/spectreconsole/spectre.console/releases)._

## 0.55.2

## What's Changed

* Support variation selectors, ZWJ sequences and surrogate pairs in
length calculation by @​fabsenet in
spectreconsole/spectre.console#2082
* Add default value to selection prompt and multiselection prompt by
@​AntekOlszewski in
spectreconsole/spectre.console#2079

## New Contributors
* @​fabsenet made their first contribution in
spectreconsole/spectre.console#2082

**Full Changelog**:
spectreconsole/spectre.console@0.55.1...0.55.2

## 0.55.1

## What's Changed

* Add tests to verify public API by @​patriksvensson in
spectreconsole/spectre.console#2073
* use StringComparer.OrdinalIgnoreCase as default comparer for
TextPrompt by @​AntekOlszewski in
spectreconsole/spectre.console#2077
* Fix markup link rendering regression by @​patriksvensson in
spectreconsole/spectre.console#2084
* Add VS16 suffix to non-presentation emojis by @​patriksvensson in
spectreconsole/spectre.console#2087
* Ensure rendered exceptions take up minimal space by @​patriksvensson
in spectreconsole/spectre.console#2089
* Fix link parsing to terminate properly by @​zhuman in
spectreconsole/spectre.console#2091

## New Contributors
* @​zhuman made their first contribution in
spectreconsole/spectre.console#2091

**Full Changelog**:
spectreconsole/spectre.console@0.55.0...0.55.1

## 0.55.0

This release brings new features, performance improvements, bug fixes,
and some important architectural changes.

> [!CAUTION]
> There are breaking changes in this release, so make sure you review
the release notes and try things out before upgrading in production.

## New Spectre.Console.Ansi Library

One of the biggest changes in this release is the introduction of 

[Spectre.Console.Ansi](https://www.nuget.org/packages/spectre.console.ansi),
a new standalone library for writing ANSI escape
sequences to the terminal without taking a full dependency on
`Spectre.Console`.

This makes it easy to add ANSI support to lightweight tools and
libraries where
pulling in the full Spectre.Console package would be overkill.
Spectre.Console
itself now depends on this library internally.

We've also added some nice convenience methods for the .NET Console
class:

```csharp
using Spectre.Console.Ansi;

Console.Markup("[yellow]Hello[/] ");
Console.MarkupLine("[blue]World[/]");
  
Console.Ansi(writer => writer
    .BeginLink("https://spectreconsole.net", linkId: 123)
    .Decoration(Decoration.Bold | Decoration.Italic)
    .Foreground(Color.Yellow)
    .Write("Spectre Console")
    .ResetStyle()
    .EndLink());
```

## Style Is Now a Struct

`Style` has been converted from a class to a struct, and link/URL
information
has been extracted into a separate `Link` type. This improves allocation
performance, especially in rendering-heavy scenarios, but is a breaking
change
for code that relies on reference semantics.

## Progress Improvements

The `Progress` widget received a lot of love in this release. It now
uses
`TimeProvider` instead of the wall clock, making it significantly easier
to
write deterministic tests. `ProgressTask` has a new `Tag` property for
attaching
arbitrary metadata, and you can now override the global
hide-when-completed
behavior on individual tasks. Tasks can also be removed from the
progress
context entirely.

Speed calculations have been improved with configurable max sampling age
and
 ... (truncated)

Commits viewable in [compare
view](spectreconsole/spectre.console@0.54.0...0.55.2).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Spectre.Console&package-manager=nuget&previous-version=0.54.0&new-version=0.55.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/19497)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some characters break table output

3 participants