Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@tomlm
Copy link
Collaborator

@tomlm tomlm commented Oct 21, 2025

This is to make Consolonia more robust for dealing with complex emoji surrogates. issue: #477

Issue

some characters are ambiguous as to whether they are rendered as single size or double size. For compatibility reasons most terminals default to an ambiguous undefined behavior. (See microsoft/terminal#16852). To give authors more control over interpretation of ambiguous sequences unicode/terminals support Variation selectors

  • Text Variation (U+FE0E) - previous character should be interpreted as text, which usually means single wide
  • Emoji Variation (U+FE0F) - previous character should be interpreted as emoji, which usually means double wide

In addition, some terminals will advance the cursor 2 characters when rendering wide chars, and some (Gnome ubuntu terminal) advance the cursor always one character.

Changes

  • Changes MeasureText() to honor Text/Emoji variation when calculating widths
  • Changes GetGlyphs() to pass through Text/Emoji variations so that Glyphs contain them
  • Changes Symbol() to keep variation if it's passed, but if it's not, it will automatically add the Emojii Variation to characters we know are Emojii's or think should be wide.
  • Changes static Icons so that if they are all rendered as double chars, either because they are already recognized emojii's, or because we have added the Emoji Variation to the icon static template.
  • Added detection of fallback (on linux) for SupportsEmojiVariations. Linux renders them differently but only advances 1 char. When SupportsEmojiVariations == false then we explicitly advance the cursor for wide chars to make it match our avalonia rendering layout.
  • Added handling for Skin variations, keycap and regional annotations.
  • New glyph unit tests
  • Updated unit tests

Effects

This has the effect of:

  • Defaults for emojiis are always wide, an we pass that on to the terminal so it can know to render it wide.
  • If a character doesn't render the way you want, you can add the variation selector to get it do that.
   var wide = $"🠷\uFE0F"; 
   var narrow = $"🠷\uFE0E"; 
   <TextBlock>🠷&#xFE0E; Wide</TextBlock>
   <TextBlock>🠷&#xFE0E; Narrow</TextBlock>

Icons before changes, showing how chars overlap with following character:
image

Icons after changes:
image

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 21, 2025

πŸ“ Walkthrough

Walkthrough

Adds grapheme-aware emoji handling and rendering across the codebase. Introduces Grapheme.Parse and uses graphemes in TextShaper and Text rendering; ControlUtils emoji/ZWJ/regional-indicator logic is reworked. Symbol and SymbolConverter gain variation-selector signaling and caching; PixelBuffer.PrintBuffer and DrawingContextImpl move to per-grapheme/per-glyph iteration and drawing. Replaces the old GlyphTypeface with a new ConsoleTypeface implementation. Console capability surface gains SupportsEmojiVariation with detection and a fast-path in console outputs. Numerous XAML icon glyphs updated and tests adjusted to expect variation selectors. Public APIs are largely additive.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to pay extra attention:

  • Grapheme.Parse and emoji/ZWJ/regional-indicator handling (Consolonia.Core.Helpers.Grapheme, ControlUtils.cs)
  • Rendering pipeline changes and glyph metrics integration (DrawingContextImpl, TextShaper, ConsoleTypeface; removal of GlyphTypeface)
  • Symbol/SymbolConverter behavior and Symbol caching for variation selectors
  • PixelBuffer.PrintBuffer loop correctness and DrawGlyphInfo per-glyph drawing
  • Console capability detection and fast-paths in AnsiConsoleOutput / DefaultNetConsoleOutput
  • Updated tests referencing variation selectors to ensure expected behavior

Possibly related PRs

Suggested reviewers

  • jinek
  • goe5mu

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
βœ… Passed checks (2 passed)
Check name Status Explanation
Title Check βœ… Passed The pull request title "Add support for Text and Emoji variation annotations" is fully related to the primary objective of the changeset. The PR introduces comprehensive support for Unicode variation selectors (U+FE0E for text and U+FE0F for emoji) across multiple components, including updates to MeasureText(), Symbol handling, console capabilities, and icon templates. The title accurately captures this main change in clear, concise language without vague terms or extraneous noise, and a developer scanning the history would immediately understand that this PR adds emoji variation annotation support.
Description Check βœ… Passed The pull request description is clearly related to the changeset and provides meaningful context about the changes. It explains the underlying issue (#477) regarding ambiguous character widths, defines the variation selectors being supported, details the specific changes made across components like MeasureText(), Symbol, and icon templates, and describes the intended effects with usage examples. The description is not vague or genericβ€”it provides specific, actionable information that directly corresponds to the modifications present in the codebase.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tomlm/AmibigiousChars

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Consolonia.Gallery/Gallery/GalleryViews/GalleryIcons.axaml (1)

31-39: Inconsistent separator: missing after β€œInfo”.

Every row adds a β€œ|” except the β€œInfo” row. Add for visual consistency and to keep width debugging consistent.

-                <TextBlock Text="{StaticResource Icon_Info}" />
+                <TextBlock Text="{StaticResource Icon_Info}" />
+                <TextBlock Text="|"/>
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)

41-46: Negative width cast to byte can overflow to 255 (control chars). Clamp before cast.

UnicodeCalculator.GetWidth(ch) returns -1 for controls; direct cast to byte corrupts Width.

-            Width = (byte)UnicodeCalculator.GetWidth(ch);
+            int w = UnicodeCalculator.GetWidth(ch);
+            Width = (byte)Math.Max(0, w);
🧹 Nitpick comments (12)
src/Consolonia.Themes/Modern/Controls/MenuItem.axaml (1)

16-16: Consider adding text variation selector for consistent narrow rendering.

The checkmark character (U+2713) can render as either text-style (narrow) or emoji-style (wide) depending on the terminal. Since this is a menu indicator where space efficiency matters, consider appending the text variation selector (U+FE0E) to ensure consistent single-width rendering: "βœ“\uFE0E".

This aligns with the PR's goal of using variation selectors to explicitly control character width.

Apply this diff to add the text variation selector:

-                        Value="βœ“" />
+                        Value="βœ“&#xFE0E;" />

Alternatively, verify that the automatic emoji detection in Symbol() (mentioned in the PR) handles this character appropriately for the menu context.

src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBlock.axaml (1)

51-51: Consider demonstrating explicit variation selectors.

The added line effectively tests automatic emoji variation detection for ambiguous-width symbols. However, since the PR introduces the ability for authors to explicitly control width using variation selectors (U+FE0E for text, U+FE0F for emoji), consider adding additional TextBlock examples that demonstrate this capability.

Apply this diff to add explicit variation selector demonstrations:

 <TextBlock Text="0123456789012345678901234567890" />
+<!-- Testing automatic emoji variation detection -->
 <TextBlock Text="☰Xβš™Xβ˜…XβŒ‚Xβ™«Xβ˜€Xβ˜•X☎X❄X☠X☷X"/>
+<!-- Forcing text presentation (U+FE0E) -->
+<TextBlock Text="☰︎Xβš™οΈŽXβ˜…οΈŽXβŒ‚οΈŽXβ™«οΈŽXβ˜€οΈŽXβ˜•οΈŽX☎︎Xβ„οΈŽX☠︎X☷︎X"/>
+<!-- Forcing emoji presentation (U+FE0F) -->
+<TextBlock Text="☰️Xβš™οΈXβ˜…οΈXβŒ‚οΈX♫️Xβ˜€οΈXβ˜•οΈX☎️X❄️X☠️X☷️X"/>
 <TextBlock Text="π“π“˜π“»π“˜π“»π“Ÿ π’»π“Ÿ, π„ž, 🎡, πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦,  πŸ³οΈβ€πŸŒˆ" />

This would provide a comprehensive demonstration of the variation selector functionality described in the PR objectives.

src/Tests/Consolonia.Gallery.Tests/CheckBoxTests.cs (1)

17-24: Consider tightening the regex patterns.

The change from .+ to .* makes the assertions more lenient by allowing zero characters between the checkbox symbols and labels. While this may accommodate layout changes from variation selectors, .* can match unintended strings and reduce test precision.

Consider using * (zero or more spaces) if the change is to accommodate whitespace variations, or .{1,3} if you expect 1-3 characters, to maintain better test validation.

Also applies to: 30-37

src/Consolonia.Gallery/Gallery/GalleryViews/GalleryIcons.axaml (1)

13-17: Consider a single reusable template or style for separators.

This repeats hundreds of times. Prefer an ItemTemplate with a trailing separator or a Style with a DataTrigger to add a separator element, reducing duplication and future drift.

Also applies to: 19-23, 25-29, 35-40, 41-46, 47-52, 53-58, 59-64, 65-70, 71-76, 77-82, 83-88, 89-94, 95-100, 101-106, 107-112, 113-118, 119-124, 125-130, 131-136, 137-142, 143-148, 149-154, 155-160, 161-166, 167-172, 173-178, 179-184, 185-190, 191-196, 201-206, 207-212, 213-218, 219-224, 225-230, 231-236, 237-242, 243-248, 249-254, 255-260, 261-266, 267-272, 273-278, 279-284, 285-290, 291-296, 297-302, 303-308, 309-314, 315-320, 321-326, 327-332, 333-338, 339-344, 345-350, 351-356, 357-362, 367-372, 373-378, 379-384, 385-390, 391-396, 397-402, 403-408, 409-414, 415-420, 421-426, 427-432, 433-438, 439-450, 451-456, 461-466, 467-472, 473-478, 479-484, 485-490, 491-496, 497-502, 503-508, 509-514, 515-520, 521-526, 527-532, 533-538, 539-544, 545-550, 551-556, 557-562, 563-568, 569-574, 575-580, 581-586, 587-592, 597-602, 603-608, 609-614, 615-620, 621-626, 627-632, 633-638, 639-644, 645-650, 651-656, 657-662, 663-668, 669-674, 675-680, 681-686, 691-696, 697-702, 703-708, 709-714, 715-720, 721-726, 727-732, 733-738, 739-744, 749-754, 755-760, 761-766, 767-772, 773-778, 779-784, 785-790, 791-796, 801-806, 807-812, 813-818, 819-824, 825-830, 831-836, 841-846, 847-852, 853-858, 859-864, 865-870, 871-876, 877-882, 883-888, 889-894, 899-904, 905-910, 911-916, 917-922, 923-928, 929-934, 935-940, 941-946, 947-952, 953-958, 959-964, 965-970, 975-980, 981-986, 987-992, 993-998, 999-1004, 1005-1010, 1011-1016, 1017-1022, 1023-1028, 1029-1034, 1035-1040, 1045-1050, 1051-1056, 1057-1062, 1063-1068, 1069-1074, 1075-1080, 1081-1086, 1087-1092, 1099-1104, 1105-1110, 1111-1116, 1117-1122, 1123-1128, 1129-1134, 1135-1140, 1141-1146, 1147-1152, 1153-1158, 1159-1164, 1165-1168, 1174-1178, 1179-1184, 1185-1190, 1191-1196, 1197-1202, 1203-1208, 1209-1214, 1215-1220, 1221-1226, 1227-1232, 1233-1238, 1239-1244, 1245-1250, 1251-1256, 1257-1262, 1263-1268, 1269-1274, 1275-1280, 1281-1286, 1287-1292, 1293-1298, 1299-1304

src/Tests/Consolonia.Gallery.Tests/RadioButtonTests.cs (1)

26-26: Leftover TODO.

β€œtodo: check why does not react to Space” β€” track via issue or resolve before merge to avoid stale comments.

src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs (2)

89-102: Width‑aware traversal is correct; handle carets on zero‑width cells too.

Currently, caret renders only when pixel.Width > 0. If caret lands on a continuation cell (width 0), it won’t draw. Add a fallback branch for pixel.IsCaret() when width ≀ 0.

-                    if (pixel.Width > 0)
+                    if (pixel.Width > 0)
                     {
                         string text = pixel.IsCaret() ? "Ꮖ" : pixel.Foreground.Symbol.GetText();
                         
                         //todo: check why cursor is not drawing
                         stringBuilder.Append(text);
                         i += pixel.Width;
                     }
-                    else
-                        i++;
+                    else
+                    {
+                        if (pixel.IsCaret())
+                        {
+                            stringBuilder.Append("Ꮖ");
+                        }
+                        i++;
+                    }

94-97: Replace TODO with tracked issue or actionable comment.

Keeps codebase clean and discoverable.

src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

107-122: Add combining mark handling (e.g., keycap U+20E3).

Nonspacing/enclosing marks should extend the previous glyph (or current emoji buffer), not form a new glyph.

+                    else if (Rune.GetUnicodeCategory(runes.Current) is
+                             System.Globalization.UnicodeCategory.NonSpacingMark or
+                             System.Globalization.UnicodeCategory.EnclosingMark)
+                    {
+                        if (emoji.Length > 0)
+                            emoji.Append(runes.Current);
+                        else if (glyphs.Count > 0)
+                            glyphs[^1] = glyphs[^1] + runes.Current;
+                        else
+                            glyphs.Add(runes.Current.ToString()); // fallback
+                    }

Add at file top:

+using System.Globalization;
src/Consolonia.Controls/ControlUtils.cs (1)

47-66: Guard against width underflow when subtracting lastWidth.

ushort subtraction can underflow if sequences are malformed/unexpected. Clamp or compute with a signed accumulator.

-                        width -= lastWidth;
+                        width = (ushort)(width > lastWidth ? width - lastWidth : 0);

Alternatively, switch width/lastWidth to int and clamp on return.

src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)

47-61: Don’t append FE0F to every width-2 character (e.g., CJK). Gate on Emoji only.

FE0F is defined for emoji-presenting code points; adding to generic wide chars is non-standard and bloats Complex.

-            // if we think it should be wide, OR we know it's an emoji 
-            if (Width == 2 || Emoji.IsEmoji(new(ch, 1)))
+            // Only force emoji variation for emoji code points
+            if (Emoji.IsEmoji(new(ch, 1)))
             {
                 // we want to use EmojiVariation to signal we think it's wide.
                 Character = char.MinValue;
                 Width = 2;
-                if (GlyphCharCache.TryGetValue(ch, out var wideChar))
-                {
-                    Complex = wideChar;
-                }
-                else
-                {
-                    Complex = GlyphCharCache[ch] = $"{ch}{EmojiVariation}";
-                }
+                Complex = GlyphCharCache.GetOrAdd(ch, c => $"{c}{EmojiVariation}");
             }

Optional: add a unit test with a CJK char (e.g., "ζΌ’") to confirm no FE0F is appended.

src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2)

146-161: Assert Complex normalization explicitly and consider CJK negative case.

The FE0F-append assertion is good. Please add a negative test to ensure FE0F is not appended for non-emoji wide chars (e.g., "ζΌ’") if you adopt the gating suggested in Symbol(char).


175-199: Minor doc nit and an extra assert suggestion.

  • Comment says β€œMix of emojis with variation selectors” but the test name says β€œWithout Variation Selectors.” Adjust comment for clarity.
  • Optional: also assert new Symbol(g).Width to pin width expectations.
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 6c6374b and ac2d4bb.

πŸ“’ Files selected for processing (17)
  • src/Consolonia.Controls/ControlUtils.cs (1 hunks)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs (2 hunks)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (5 hunks)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1 hunks)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs (1 hunks)
  • src/Consolonia.Gallery/Gallery/GalleryViews/GalleryIcons.axaml (15 hunks)
  • src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBlock.axaml (1 hunks)
  • src/Consolonia.Themes/Modern/Controls/MenuItem.axaml (1 hunks)
  • src/Consolonia.Themes/Modern/ModernBase.axaml (1 hunks)
  • src/Consolonia.Themes/Templates/Controls/Icons.axaml (7 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelBufferTests.cs (4 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelForegroundTests.cs (2 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/SimpleSymbolTests.cs (3 hunks)
  • src/Tests/Consolonia.Gallery.Tests/CheckBoxTests.cs (1 hunks)
  • src/Tests/Consolonia.Gallery.Tests/RadioButtonTests.cs (1 hunks)
  • src/Tests/Consolonia.Gallery.Tests/SliderTests.cs (1 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (2)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Tests/Consolonia.Gallery.Tests/RadioButtonTests.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelForegroundTests.cs
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs
  • src/Consolonia.Controls/ControlUtils.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelBufferTests.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/SimpleSymbolTests.cs
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
  • src/Tests/Consolonia.Gallery.Tests/SliderTests.cs
  • src/Tests/Consolonia.Gallery.Tests/CheckBoxTests.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs
src/Tests/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

Write tests using NUnit (project uses NUnit with custom Consolonia test base classes)

Files:

  • src/Tests/Consolonia.Gallery.Tests/RadioButtonTests.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelForegroundTests.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelBufferTests.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/SimpleSymbolTests.cs
  • src/Tests/Consolonia.Gallery.Tests/SliderTests.cs
  • src/Tests/Consolonia.Gallery.Tests/CheckBoxTests.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
🧠 Learnings (2)
πŸ“š Learning: 2024-12-05T18:23:58.201Z
Learnt from: jinek
PR: jinek/Consolonia#176
File: src/Consolonia.Themes.TurboVision/Templates/Controls/MenuItem.axaml:131-131
Timestamp: 2024-12-05T18:23:58.201Z
Learning: In the Avalonia UI project, within `src/Consolonia.Themes.TurboVision/Templates/Controls/MenuItem.axaml`, MenuItem controls are intentionally designed without a `:pointerover` state to maintain touch-friendliness and consistency across controls.

Applied to files:

  • src/Consolonia.Themes/Modern/Controls/MenuItem.axaml
πŸ“š Learning: 2024-12-05T18:31:27.161Z
Learnt from: jinek
PR: jinek/Consolonia#176
File: src/Consolonia.Themes.TurboVision/Templates/Controls/MenuItem.axaml:131-131
Timestamp: 2024-12-05T18:31:27.161Z
Learning: In `src/Consolonia.Themes.TurboVision/Templates/Controls/MenuItem.axaml`, MenuItem controls do not react to mouse-over events due to performance reasons and simplicity. Touch-screen support is planned for the future.

Applied to files:

  • src/Consolonia.Themes/Modern/Controls/MenuItem.axaml
🧬 Code graph analysis (4)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelForegroundTests.cs (1)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)
  • Symbol (170-181)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-85)
src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1)
  • Symbol (8-24)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-85)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)
  • IReadOnlyList (70-147)
src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs (3)
src/Consolonia.Core/Drawing/RenderTarget.cs (1)
  • Pixel (94-104)
src/Consolonia.Core/Drawing/PixelBufferImplementation/CaretStyleExtensions.cs (2)
  • IsCaret (16-19)
  • IsCaret (21-24)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)
  • Symbol (170-181)
  • GetText (157-162)
πŸͺ› GitHub Actions: General Build
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs

[error] 157-157: CA1024: Use properties where appropriate (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1024)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: inspection
πŸ”‡ Additional comments (15)
src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1)

32-35: LGTM!

The added branches properly handle Character serialization when it's not MinValue, and provide a null fallback for symbols with no representation. This ensures complete JSON serialization coverage for all Symbol states.

src/Consolonia.Themes/Templates/Controls/Icons.axaml (3)

14-31: LGTM!

The navigation icon updates consistently apply emoji variation selectors (U+FE0F) and select appropriate glyphs to ensure double-width rendering where intended. The changes align with the PR's goal of explicit width control.


34-60: LGTM!

Action icon resources consistently apply emoji variation selectors for double-width rendering. The systematic updates across Add, Remove, Close, Edit, and other action icons align with the PR's variation selector support.


73-75: LGTM!

The remaining icon updates consistently apply emoji variation selectors across communication, file, media, technology, and misc categories. These changes ensure consistent double-width rendering for emoji-style icons.

Also applies to: 83-83, 98-99, 112-112, 177-177, 219-220

src/Consolonia.Themes/Modern/ModernBase.axaml (1)

24-24: LGTM!

The text variation selector (U+FE0E) correctly forces single-width presentation for the slider thumb, which is appropriate for a control where precise width matters. This contrasts with the emoji variation selector (U+FE0F) used for double-width icons elsewhere.

src/Tests/Consolonia.Gallery.Tests/SliderTests.cs (1)

17-17: LGTM!

Test expectations correctly updated to include the text variation selector (U+FE0E) added to the slider thumb symbol in ModernBase.axaml, ensuring tests pass with the new rendering behavior.

Also applies to: 20-20

src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/SimpleSymbolTests.cs (1)

42-42: LGTM!

Test expectations correctly updated to verify that Symbol construction automatically adds the emoji variation selector (U+FE0F) to emoji characters, ensuring they render as double-width. This validates the core behavior introduced in this PR.

Also applies to: 52-52, 152-152

src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelForegroundTests.cs (1)

82-82: LGTM!

Test expectations correctly validate that PixelForeground construction and blending operations preserve emoji variation selectors (U+FE0F), ensuring consistent double-width rendering throughout the pixel rendering pipeline.

Also applies to: 154-154

src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelBufferTests.cs (1)

35-35: LGTM!

Test expectations comprehensively validate that PixelBuffer correctly stores and retrieves emoji with the emoji variation selector (U+FE0F), covering single emoji, multiple emoji, and complex multi-codepoint sequences. The width assertions (2) correctly verify double-width rendering.

Also applies to: 106-106, 140-142, 195-195

src/Tests/Consolonia.Gallery.Tests/RadioButtonTests.cs (1)

16-23: Regex change from .+ to .* looks good.

Loosens matching to tolerate zero‑width/variation selectors. Please also confirm AssertHasText doesn’t use Singleline/DotAll; we don’t want .* to span lines inadvertently.

Also applies to: 27-29

src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

81-100: Overall logic aligns with PR goals.

Emoji start/flush behavior and variation selector attachment are coherent; end‑flush is correct.

Please add tests for:

  • Stray ZWJ/ORC between non‑emoji text.
  • Keycap sequences (e.g., β€œ1️⃣”).

Also applies to: 125-135, 145-146

src/Consolonia.Controls/ControlUtils.cs (1)

44-56: Emoji/variation width adjustments look correct.

Heuristic (emoji=2, text=1 with VS overrides) matches PR intent. Keeping lastWidth in sync on VS is good.

Also applies to: 57-66, 67-71

src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)

30-38: Parameterless ctor LGTM.

Consistent initialization of empty symbol and Width = 0.

src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2)

109-126: Good coverage for text presentation selector (FE0E).

Covers simple, emoji, and complex emoji inputs; assertions look correct.


128-144: Good coverage for emoji presentation selector (FE0F).

Validates width=2 and glyph grouping; looks correct.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)

21-26: Do not lock on the dictionary; use a separate lock object and add StringComparer.Ordinal.

Thread safety was partially addressed by adding locks, but locking on the collection itself is problematic (exposes the lock object publicly via the field). Additionally, GlyphComplexCache should specify StringComparer.Ordinal for consistent, culture-invariant string comparison.

Apply this diff to fix both issues:

-        private const char TextVariation = '\ufe0e';
-        private const char EmojiVariation = '\ufe0f';
+        private const char TextVariation = '\ufe0e';
+        private const char EmojiVariation = '\ufe0f';
+        
+        private static readonly object GlyphCharCacheLock = new();
+        private static readonly object GlyphComplexCacheLock = new();

-        private static readonly Dictionary<char, string> GlyphCharCache = new();
-        private static readonly Dictionary<string, string> GlyphComplexCache = new();
+        private static readonly Dictionary<char, string> GlyphCharCache = new();
+        private static readonly Dictionary<string, string> GlyphComplexCache = new(StringComparer.Ordinal);

Then update lock statements:

-                lock (GlyphCharCache)
+                lock (GlyphCharCacheLock)
-                lock (GlyphComplexCache)
+                lock (GlyphComplexCacheLock)

Alternatively, use ConcurrentDictionary with GetOrAdd as suggested in the previous review to avoid explicit locking.

🧹 Nitpick comments (2)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)

95-95: Variable shadowing: lambda parameter hides outer glyph.

The lambda parameter glyph shadows the constructor parameter of the same name, reducing readability.

Rename the lambda parameter:

-                else if (glyph.Any(glyph => glyph == TextVariation || glyph == EmojiVariation))
+                else if (glyph.Any(ch => ch == TextVariation || ch == EmojiVariation))

167-169: Optional: simplify Complex check.

Based on constructor logic, Complex is always null or has length β‰₯ 2. The condition Complex.Length > 1 is defensive but redundant.

You can simplify to:

-            if (Width == 0)
-                return String.Empty;
-            return Complex != null && Complex.Length > 1 ? Complex : new(Character, 1);
+            if (Width == 0)
+                return String.Empty;
+            return Complex ?? new string(Character, 1);

The current code is correct and defensive, so this is purely a style preference.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 7bc7493 and 45f48eb.

πŸ“’ Files selected for processing (2)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (5 hunks)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs (1 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Helpers/UtilityExtensions.cs
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
🧬 Code graph analysis (1)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-85)
src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1)
  • Symbol (8-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: inspection
  • GitHub Check: build
πŸ”‡ Additional comments (3)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (3)

81-100: LGTM: Emoji assembly logic is sound.

The rewritten logic correctly:

  • Detects emoji via Emoji.IsEmoji.
  • Continues building when the previous rune was a joiner (ZWJ/ORC).
  • Flushes the existing emoji buffer before starting a new emoji sequence.

This ensures complex emoji sequences are properly accumulated into single glyphs.


101-114: LGTM: Stray joiner handling is correct.

This logic correctly addresses the past review concern. ZWJ/ORC is now:

  • Appended to the emoji buffer if one is in progress.
  • Appended to the last glyph if no emoji is being built but glyphs exist.
  • Ignored if neither (preventing standalone joiner glyphs).

Based on learnings


115-130: LGTM: Variation selector handling aligns with PR goals.

Variation selectors (U+FE0E/U+FE0F) are correctly:

  • Appended to the emoji buffer when building a complex emoji.
  • Appended to the last glyph when no emoji is in progress.

This ensures variation selectors are preserved as part of the glyph, enabling text vs. emoji presentation control.

@tomlm tomlm changed the title Add support for Text and Emoji variations Add support for Text and Emoji variation annotations Oct 21, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)

24-25: Make glyph caches lock‑free and ordinal with ConcurrentDictionary.

Current locking works but adds contention; use GetOrAdd with ordinal comparer.

+using System.Collections.Concurrent;
@@
-        private static readonly Dictionary<char, string> GlyphCharCache = new();
-        private static readonly Dictionary<string, string> GlyphComplexCache = new();
+        private static readonly ConcurrentDictionary<char, string> GlyphCharCache = new();
+        private static readonly ConcurrentDictionary<string, string> GlyphComplexCache = new(StringComparer.Ordinal);
@@
-                lock (GlyphCharCache)
-                {
-                    if (GlyphCharCache.TryGetValue(ch, out var wideChar))
-                    {
-                        Complex = wideChar;
-                    }
-                    else
-                    {
-                        Complex = GlyphCharCache[ch] = $"{ch}{EmojiVariation}";
-                    }
-                }
+                Complex = GlyphCharCache.GetOrAdd(ch, c => $"{c}{EmojiVariation}");
@@
-                    Character = char.MinValue;
-                    lock (GlyphComplexCache)
-                    {
-                        if (GlyphComplexCache.TryGetValue(glyph, out var complex))
-                        {
-                            Complex = complex;
-                        }
-                        else
-                        {
-                            // use text variation for narrow glyphs, emoji variation for wide glyphs
-                            if (Width == 1)
-                                Complex = GlyphComplexCache[glyph] = $"{glyph}{TextVariation}";
-                            else
-                                Complex = GlyphComplexCache[glyph] = $"{glyph}{EmojiVariation}";
-                        }
-                    }
+                    Character = char.MinValue;
+                    var w = Width;
+                    Complex = GlyphComplexCache.GetOrAdd(glyph, g => w == 1 ? $"{g}{TextVariation}" : $"{g}{EmojiVariation}");

Also applies to: 53-63, 104-118


86-96: Fix unsafe byte casts of MeasureText() in Symbol.cs and ConsoleCursor.cs; optimize single‑char path.

Casting ushort β†’ byte truncates silently; clamp to byte.MaxValue instead. Also optimize Symbol.cs to skip measuring single-char glyphs.

Symbol.cs (lines 86–96):

-                Width = (byte)glyph.MeasureText();
-
-                if (glyph.Length == 1)
-                {
-                    // we can use the single char constructor for optimization
-                    this = new Symbol(glyph[0]);
-                }
+                if (glyph.Length == 1)
+                {
+                    // fast path: delegate to char ctor and stop
+                    this = new Symbol(glyph[0]);
+                    return;
+                }
+
+                ushort measuredWidth = glyph.MeasureText();
+                Width = measuredWidth > byte.MaxValue ? byte.MaxValue : (byte)measuredWidth;

ConsoleCursor.cs (line 18):

-            Width = (byte)type.MeasureText();
+            ushort measuredWidth = type.MeasureText();
+            Width = measuredWidth > byte.MaxValue ? byte.MaxValue : (byte)measuredWidth;
🧹 Nitpick comments (1)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)

81-81: Use char.MinValue consistently.

Minor style nit for consistency with the rest of the file.

-            Character = Char.MinValue;
+            Character = char.MinValue;
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 45f48eb and 24926e3.

πŸ“’ Files selected for processing (1)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (5 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
🧠 Learnings (1)
πŸ“š Learning: 2024-11-25T06:18:08.175Z
Learnt from: tomlm
PR: jinek/Consolonia#153
File: src/Consolonia.Core/Controls/DialogWindow.cs:136-141
Timestamp: 2024-11-25T06:18:08.175Z
Learning: In the `DialogWindow` class (`src/Consolonia.Core/Controls/DialogWindow.cs`), casting `Width` and `Height` to `ushort` is intentional and should not be flagged as an issue.

Applied to files:

  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
🧬 Code graph analysis (1)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-85)
src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1)
  • Symbol (8-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: inspection
πŸ”‡ Additional comments (1)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)

163-166: CA1024 suppression: LGTM.

Pragma‑based suppression is appropriate for this debug‑only helper.

If you prefer attribute suppression, swap pragma for:

[SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Debug-only helper; allocates")]
public string GetText() { ... }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)

39-59: Guard against negative widths (control chars) to prevent 255 wrap.

UnicodeCalculator.GetWidth can return negatives for control chars; casting to byte yields 255. Clamp and align tab width to 4 (as in MeasureText).

-            Width = (byte)UnicodeCalculator.GetWidth(ch);
+            int w = UnicodeCalculator.GetWidth(ch);
+            if (w < 0)
+            {
+                // Align with ControlUtils.MeasureText: tab => 4, other controls => 0
+                w = ch == '\t' ? 4 : 0;
+            }
+            Width = (byte)w;
♻️ Duplicate comments (2)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)

22-24: Make static caches thread‑safe and lock‑free with ConcurrentDictionary.

Rendering can construct Symbols on multiple threads. Replace manual locks on Dictionary with ConcurrentDictionary and GetOrAdd; use Ordinal comparer for string keys.

-using System.Collections.Generic;
+using System.Collections.Generic;
+using System.Collections.Concurrent;
@@
-        private static readonly Dictionary<char, string> GlyphCharCache = new();
-        private static readonly Dictionary<string, string> GlyphComplexCache = new();
+        private static readonly ConcurrentDictionary<char, string> GlyphCharCache = new();
+        private static readonly ConcurrentDictionary<string, string> GlyphComplexCache = new(StringComparer.Ordinal);
@@
-                lock (GlyphCharCache)
-                {
-                    if (GlyphCharCache.TryGetValue(ch, out string wideChar))
-                        Complex = wideChar;
-                    else
-                        Complex = GlyphCharCache[ch] = $"{ch}{EmojiVariation}";
-                }
+                Complex = GlyphCharCache.GetOrAdd(ch, c => $"{c}{EmojiVariation}");
@@
-                    lock (GlyphComplexCache)
-                    {
-                        if (GlyphComplexCache.TryGetValue(glyph, out string complex))
-                        {
-                            Complex = complex;
-                        }
-                        else
-                        {
-                            // use text variation for narrow glyphs, emoji variation for wide glyphs
-                            if (Width == 1)
-                                Complex = GlyphComplexCache[glyph] = $"{glyph}{TextVariation}";
-                            else
-                                Complex = GlyphComplexCache[glyph] = $"{glyph}{EmojiVariation}";
-                        }
-                    }
+                    var w = Width; // capture
+                    Complex = GlyphComplexCache.GetOrAdd(glyph, g => w == 1 ? $"{g}{TextVariation}" : $"{g}{EmojiVariation}");

Also applies to: 51-57, 98-112


82-82: Clamp MeasureText() (ushort) to byte to avoid truncation.

Casting can silently wrap above 255. Clamp, or consider widening Width to ushort in a future API change.

-                Width = (byte)glyph.MeasureText();
+                ushort measured = glyph.MeasureText();
+                Width = measured > byte.MaxValue ? byte.MaxValue : (byte)measured;
🧹 Nitpick comments (1)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

81-81: Minor: avoid per-rune string allocation.

Cache runes.Current.ToString() once per iteration to reduce allocations on long inputs.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 24926e3 and 3a637c0.

πŸ“’ Files selected for processing (8)
  • src/Consolonia.Controls/ControlUtils.cs (1 hunks)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs (1 hunks)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (5 hunks)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1 hunks)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs (1 hunks)
  • src/Consolonia.Gallery/Gallery/GalleryViews/GalleryIcons.axaml (15 hunks)
  • src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBlock.axaml (1 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
  • src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBlock.axaml
  • src/Consolonia.Controls/ControlUtils.cs
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
🧠 Learnings (1)
πŸ“š Learning: 2024-11-25T06:18:08.175Z
Learnt from: tomlm
PR: jinek/Consolonia#153
File: src/Consolonia.Core/Controls/DialogWindow.cs:136-141
Timestamp: 2024-11-25T06:18:08.175Z
Learning: In the `DialogWindow` class (`src/Consolonia.Core/Controls/DialogWindow.cs`), casting `Width` and `Height` to `ushort` is intentional and should not be flagged as an issue.

Applied to files:

  • src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
🧬 Code graph analysis (2)
src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs (2)
src/Consolonia.Core/Drawing/PixelBufferImplementation/CaretStyleExtensions.cs (2)
  • IsCaret (16-19)
  • IsCaret (21-24)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)
  • Symbol (172-183)
  • GetText (158-164)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-86)
src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1)
  • Symbol (8-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: inspection
πŸ”‡ Additional comments (5)
src/Consolonia.Gallery/Gallery/GalleryViews/GalleryIcons.axaml (2)

30-34: Verify the missing delimiter for "Info" entry.

The "Info" icon entry is the only one across all categories that doesn't include the vertical delimiter "|" after the icon TextBlock. All other 150+ entries consistently have this delimiter.

Is this intentional, or should "Info" also include the delimiter for consistency?


16-1304: LGTM! Delimiter approach aligns with PR objectives.

Adding vertical delimiters after each icon provides a clear visual indicator for verifying that emoji and text variation width handling works correctly. If any icon renders wider than its measured width (e.g., emoji presentation not properly detected), it will visibly overlap the delimiter, making width calculation issues immediately apparent in the gallery view.

This is an effective testing strategy for the variation selector functionality introduced in this PR.

src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs (1)

89-103: LGTM: iteration now respects variable pixel widths.

Advancing by pixel.Width and emitting text only for width>0 correctly handles wide/zero-width cells and avoids double-printing trailing halves. Good alignment with Symbol.Width and Complex glyphs.

Consider a quick test: a row with [2,0,1,2,0] widths prints exactly 3 glyphs and terminates (no infinite loop).

src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (2)

45-58: Adding FE0F for all width==2 chars may be non‑conformant on non‑emoji wide code points.

FE0F is defined for specific bases; appending it to generic wide (e.g., CJK) may be ignored or render inconsistently across fonts/consoles. If the intent is β€œforce wide,” consider limiting FE0F insertion to Emoji.IsEmoji(...) only, or gate behind a capability flag.

Would you like a targeted guard (emoji only) or a runtime toggle for this behavior?


157-160: CA1024 suppression is appropriate here.

Debug-only string allocation; keeping a method avoids API churn. LGTM.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/Consolonia.Core/Dummy/DummyConsole.cs (1)

46-48: Consider returning a default value instead of throwing.

The DummyConsole is used in testing scenarios where code may check the SupportsEmojiVariation capability. Throwing NotImplementedException could cause test failures. Consider returning false (or true if you want to test emoji variation paths) instead.

Apply this diff:

-
-        public bool SupportsEmojiVariation => throw new System.NotImplementedException();
-
+        public bool SupportsEmojiVariation => false;
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (1)

7-7: Remove unused using directive at line 7.

The using Consolonia.Core.Text; import is not referenced anywhere in this file. Remove it to keep the imports clean.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3a637c0 and fbbf4cc.

πŸ“’ Files selected for processing (7)
  • src/Consolonia.Controls/IConsoleCapabilities.cs (1 hunks)
  • src/Consolonia.Core/Drawing/RenderTarget.cs (2 hunks)
  • src/Consolonia.Core/Dummy/DummyConsole.cs (1 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (2 hunks)
  • src/Consolonia.Core/Infrastructure/ConsoleBase.cs (1 hunks)
  • src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3 hunks)
  • src/Consolonia.NUnit/UnitTestConsole.cs (1 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Dummy/DummyConsole.cs
  • src/Consolonia.NUnit/UnitTestConsole.cs
  • src/Consolonia.Core/Infrastructure/ConsoleBase.cs
  • src/Consolonia.Core/Drawing/RenderTarget.cs
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
  • src/Consolonia.Controls/IConsoleCapabilities.cs
  • src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs
🧬 Code graph analysis (2)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (5)
src/Consolonia.Core/Dummy/DummyConsole.cs (1)
  • WriteText (69-71)
src/Consolonia.Core/Infrastructure/ConsoleBase.cs (1)
  • WriteText (191-195)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (1)
  • WriteText (74-77)
src/Consolonia.NUnit/UnitTestConsole.cs (1)
  • WriteText (72-75)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (1)
  • WriteText (83-83)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)
  • WriteText (98-101)
src/Consolonia.Core/Infrastructure/ConsoleBase.cs (1)
  • WriteText (191-195)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (1)
  • WriteText (83-83)
πŸ”‡ Additional comments (5)
src/Consolonia.NUnit/UnitTestConsole.cs (1)

33-33: LGTM!

The implementation is consistent with SupportsComplexEmoji on line 32 and appropriate for the unit test console.

src/Consolonia.Core/Infrastructure/ConsoleBase.cs (1)

134-135: LGTM!

The delegation pattern is consistent with the existing SupportsComplexEmoji property on line 132.

src/Consolonia.Core/Drawing/RenderTarget.cs (1)

100-101: LGTM!

Formatting improvement for consistency.

src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (2)

23-28: LGTM!

The nullable bool pattern with default fallback to false ensures safe behavior if detection hasn't run.


118-123: LGTM!

The detection logic correctly uses WriteText (which doesn't add a newline) to measure cursor position changes, making it more reliable than using Console.WriteLine.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (2)

91-91: Use constructor instead of tuple cast.

The cast syntax (PixelBufferCoordinate)((ushort)0, (ushort)(_headBufferPoint.Y + 1)) is non-idiomatic. Use the new constructor for clarity and consistency with line 88.

Apply this diff:

-    _headBufferPoint = (PixelBufferCoordinate)((ushort)0, (ushort)(_headBufferPoint.Y + 1));
+    _headBufferPoint = new PixelBufferCoordinate((ushort)0, (ushort)(_headBufferPoint.Y + 1));

106-106: Use constructor instead of tuple cast.

Same issue as line 91β€”use the new constructor for consistency.

Apply this diff:

-        bufferPoint = (PixelBufferCoordinate)((ushort)0, (ushort)(bufferPoint.Y + 1));
+        bufferPoint = new PixelBufferCoordinate((ushort)0, (ushort)(bufferPoint.Y + 1));
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 1c9df13 and 3816b32.

πŸ“’ Files selected for processing (1)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
🧬 Code graph analysis (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4)
src/Consolonia.Core/Text/Esc.cs (1)
  • Esc (9-189)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (74-77)
  • PixelBufferCoordinate (49-53)
  • SetCaretPosition (36-47)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (3)
  • WriteText (83-83)
  • PixelBufferCoordinate (33-33)
  • SetCaretPosition (27-27)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-86)
πŸ”‡ Additional comments (3)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)

7-7: LGTM! Required for MeasureText extension method.

The using statement is necessary for the MeasureText() extension method used in the updated Print method.


24-29: LGTM! Clear separation of concerns.

The split from a single _supportEmoji field into _supportsComplexEmoji and _supportsEmojiVariation appropriately separates two distinct terminal capabilities, making the code more maintainable.


139-144: LGTM! Well-designed terminal capability detection.

The two-stage detection correctly identifies:

  1. Complex emoji support (composite surrogate pairs with ZWJ)
  2. Variation selector support (U+FE0F rendering)

Both tests use cursor position movement as a reliable indicator of terminal rendering capabilities. The sequential dependency on left2 is appropriate.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (1)

118-124: Use Console.Write, not Console.WriteLine, for cursor-delta detection.
WriteLine adds a newline and corrupts the width measurement. Switch to Write.

-            (int left, _) = Console.GetCursorPosition();
-            Console.WriteLine("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦");
-            (int left2, _) = Console.GetCursorPosition();
+            (int left, _) = Console.GetCursorPosition();
+            Console.Write("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦");
+            (int left2, _) = Console.GetCursorPosition();
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (2)

80-92: Set the caret before writing in the emoji-variation path.
Currently writes at the head position, ignoring bufferPoint. Position first.

-            if (SupportsEmojiVariation)
+            if (SupportsEmojiVariation)
             {
+                SetCaretPosition(bufferPoint);
                 sb.Append(str);
                 sb.Append(Esc.Reset);
 
                 WriteText(sb.ToString());
                 ushort textWidth = str.MeasureText();
-                if (_headBufferPoint.X < Size.Width - textWidth)
+                if (_headBufferPoint.X < Size.Width - textWidth)
                     _headBufferPoint =
                         new PixelBufferCoordinate((ushort)(_headBufferPoint.X + textWidth), _headBufferPoint.Y);
                 else
                     _headBufferPoint = (PixelBufferCoordinate)((ushort)0, (ushort)(_headBufferPoint.Y + 1));
             }

95-112: Fix Y-coordinate and remove dead append in per-glyph path.
Use bufferPoint.Y when advancing, not _headBufferPoint.Y. Also remove unused sb.Append(str).

                 WriteText(sb.ToString());
 
                 foreach (string glyph in str.GetGlyphs(SupportsComplexEmoji))
                 {
                     SetCaretPosition(bufferPoint);
                     WriteText(glyph);
                     ushort textWidth = glyph.MeasureText();
                     if (bufferPoint.X < Size.Width - textWidth)
-                        bufferPoint =
-                            new PixelBufferCoordinate((ushort)(bufferPoint.X + textWidth), _headBufferPoint.Y);
+                        bufferPoint =
+                            new PixelBufferCoordinate((ushort)(bufferPoint.X + textWidth), bufferPoint.Y);
                     else
                         bufferPoint = (PixelBufferCoordinate)((ushort)0, (ushort)(bufferPoint.Y + 1));
                 }
 
-                sb.Append(str);
                 WriteText(Esc.Reset);
                 _headBufferPoint = bufferPoint;
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3816b32 and 0e6a3fd.

πŸ“’ Files selected for processing (3)
  • src/Consolonia.Core/Dummy/DummyConsole.cs (2 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
  • src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (2 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
  • src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs
  • src/Consolonia.Core/Dummy/DummyConsole.cs
🧬 Code graph analysis (2)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)
src/Consolonia.Core/Text/Esc.cs (1)
  • Esc (9-189)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • PixelBufferCoordinate (48-52)
  • SetCaretPosition (35-46)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-86)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (5)
src/Consolonia.Core/Dummy/DummyConsole.cs (1)
  • WriteText (70-72)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)
  • WriteText (120-123)
src/Consolonia.Core/Infrastructure/ConsoleBase.cs (1)
  • WriteText (191-195)
src/Consolonia.NUnit/UnitTestConsole.cs (1)
  • WriteText (72-75)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (1)
  • WriteText (83-83)
πŸ”‡ Additional comments (8)
src/Consolonia.Core/Dummy/DummyConsole.cs (1)

1-1: OK to add using System.
Needed previously for exceptions; harmless even if we stop throwing.

src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)

21-23: LGTM: capability fields added. Backing fields for emoji support are appropriate.


26-29: LGTM: capability properties. Virtual getters correctly expose detected state.


125-129: LGTM: variation support probe. Measuring width after writing β€œβš™οΈŽοΈβ€ is consistent with the approach above (once fixed to use Write).
Please validate on Windows Terminal vs. legacy conhost, as reported widths can differ.

src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4)

7-7: LGTM: import added. Needed for MeasureText/GetGlyphs extensions.


24-26: LGTM: nullable capability fields. Deferred detection suits ANSI path.


27-30: LGTM: capability properties. Null-coalescing to false is safe.


140-146: LGTM: detection logic. ANSI path uses WriteText (no newline), so deltas should be accurate; variation probe looks correct.
Test across terminals (Windows Terminal, macOS Terminal, Linux VTE) to confirm consistent deltas.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

80-101: Avoid per‑rune string allocations for Emoji.IsEmoji.

Using rune.ToString() allocates. If NeoSmart.Unicode exposes an overload for Rune or codepoint, use it; otherwise cache ToString() once per iteration.

Example:

- if (Emoji.IsEmoji(runes.Current.ToString()))
+ string current = runes.Current.ToString();
+ if (Emoji.IsEmoji(current))
src/Consolonia.Controls/ControlUtils.cs (1)

66-72: Modifiers/keycap extender branch: remove dead comment or normalize behavior.

This branch intentionally does nothing (continue current glyph). Replace comment with a single clarifying line to avoid confusion.

-                    {
-                        // Emoji modifier (skin tone) or keycap extender should continue current glyph
-
-                        // else: combining β€” ignore
-                    }
+                    {
+                        // Emoji modifier or keycap extender: no width change; continues prior glyph.
+                    }
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

127-148: Variation capability probe: clarify test char and tighten check.

Consider probing with a base that defaults to text (e.g., β€œβš™β€) to ensure FE0F actually widens. Current β€œπŸ—‘\uFE0F” might already default to emoji on some consoles.

-            // write out a char with wide variation selector
-            WriteText($"πŸ—‘\ufe0f");
+            // probe FE0F effect using a text-default emoji-capable base
+            WriteText($"βš™\ufe0f");

If you keep β€œπŸ—‘\uFE0F”, please verify left3-left2==2 on your target consoles.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 0e6a3fd and 31f6506.

πŸ“’ Files selected for processing (5)
  • src/Consolonia.Controls/ControlUtils.cs (1 hunks)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs (1 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
  • src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (2 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs
🧰 Additional context used
πŸ““ Path-based instructions (2)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Helpers/UtilityExtensions.cs
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
  • src/Consolonia.Controls/ControlUtils.cs
src/Tests/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

Write tests using NUnit (project uses NUnit with custom Consolonia test base classes)

Files:

  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
🧬 Code graph analysis (2)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-92)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (3)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-92)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)
  • IReadOnlyList (70-165)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/SimpleSymbolTests.cs (16)
  • Test (13-19)
  • Test (21-27)
  • Test (29-35)
  • Test (37-45)
  • Test (47-54)
  • Test (56-64)
  • Test (66-73)
  • Test (75-83)
  • Test (85-92)
  • Test (94-106)
  • Test (108-114)
  • Test (116-124)
  • Test (126-134)
  • Test (136-144)
  • Test (146-154)
  • Test (156-163)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/Consolonia.Core/Dummy/DummyConsole.cs (1)

1-1: Potentially unused using directive.

The using System; directive appears unused since no System types are referenced in this file. Consider removing it to keep imports clean.

-using System;
-
 using Avalonia.Media;
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 31f6506 and be6b19e.

πŸ“’ Files selected for processing (1)
  • src/Consolonia.Core/Dummy/DummyConsole.cs (2 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Dummy/DummyConsole.cs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: inspection
πŸ”‡ Additional comments (1)
src/Consolonia.Core/Dummy/DummyConsole.cs (1)

47-49: Past review feedback addressed correctly.

The property now returns a stable boolean value (true) consistent with SupportsComplexEmoji, resolving the previous concern about throwing NotImplementedException. This is appropriate for a dummy console implementation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

110-115: Critical: _headBufferPoint updated from stale value instead of actual write position.

Lines 111-115 update _headBufferPoint based on the old _headBufferPoint value rather than the bufferPoint where text was actually written. This breaks cursor tracking because:

  1. In the SupportsEmojiVariation branch, text is written at bufferPoint (not at _headBufferPoint)
  2. In the else branch, bufferPoint is incrementally advanced to track the actual write position
  3. But the final _headBufferPoint update ignores bufferPoint entirely

Apply this diff to use the actual write position:

             WriteText(Esc.Reset);
-            if (_headBufferPoint.X < Size.Width - textWidth)
-                _headBufferPoint =
-                    new PixelBufferCoordinate((ushort)(_headBufferPoint.X + textWidth), _headBufferPoint.Y);
-            else
-                _headBufferPoint = (PixelBufferCoordinate)((ushort)0, (ushort)(_headBufferPoint.Y + 1));
+            _headBufferPoint = bufferPoint;
♻️ Duplicate comments (4)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (2)

86-90: Critical: bufferPoint is never advanced in the SupportsEmojiVariation branch.

When SupportsEmojiVariation is true, the code writes text at bufferPoint but never updates bufferPoint to reflect the new cursor position. This causes the subsequent _headBufferPoint update (lines 111-115) to use stale data, resulting in incorrect cursor tracking.

Apply this diff:

             if (SupportsEmojiVariation)
             {
                 SetCaretPosition(bufferPoint);
                 WriteText(str);
+                bufferPoint = new PixelBufferCoordinate((ushort)(bufferPoint.X + textWidth), bufferPoint.Y);
             }

96-100: Critical: Use glyphWidth instead of textWidth for clearing.

Line 99 uses textWidth (the width of the entire string) instead of glyphWidth (the width of the current glyph). This causes the code to clear far more space than necessary for each wide glyph, potentially erasing content beyond the current glyph's bounds.

Apply this diff:

                 if (glyphWidth > 1)
                 {
                     WriteText(Esc.SetCursorPosition(bufferPoint.X, bufferPoint.Y));
-                    WriteText(new string(' ', textWidth));
+                    WriteText(new string(' ', glyphWidth));
                 }
src/Consolonia.Controls/ControlUtils.cs (1)

40-45: Gate FE0E/FE0F width toggles to valid bases (emoji or keycap) and track last rune.

Currently, any prior width-1 rune plus FE0F becomes width 2 (e.g., "x\uFE0F"), which diverges from real terminals. Only toggle width when the preceding rune is an emoji or a keycap base; track lastRune and add a simple keycap-base check. Also update lastRune each iteration.

Apply within this method:

@@
-            ushort width = 0;
-            ushort lastWidth = 0;
+            ushort width = 0;
+            ushort lastWidth = 0;
+            var lastRune = new Rune();
@@
-                    else if (rune.Value == Codepoints.VariationSelectors.EmojiSymbol &&
-                             lastWidth == 1)
+                    else if (rune.Value == Codepoints.VariationSelectors.EmojiSymbol &&
+                             lastWidth == 1 &&
+                             (Emoji.IsEmoji(lastRune.ToString()) || IsKeycapBase(lastRune.Value)))
                     {
                         // adjust for the emoji presentation, which is width 2
                         width++;
                         lastWidth = 2;
                     }
-                    else if (rune.Value == Codepoints.VariationSelectors.TextSymbol &&
-                             lastWidth == 2)
+                    else if (rune.Value == Codepoints.VariationSelectors.TextSymbol &&
+                             lastWidth == 2 &&
+                             (Emoji.IsEmoji(lastRune.ToString()) || IsKeycapBase(lastRune.Value)))
                     {
                         // adjust for the text presentation, which is width 1
                         width--;
                         lastWidth = 1;
                     }
@@
-                    if (runeWidth > 0) lastWidth = (ushort)runeWidth;
+                    if (runeWidth > 0) lastWidth = (ushort)runeWidth;
+                    // track previous rune for variation gating
+                    lastRune = rune;

Add this local helper near the end of the method (before return width;):

static bool IsKeycapBase(int v) => (v >= '0' && v <= '9') || v == '#' || v == '*';

Also applies to: 52-65, 79-79

src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

81-101: Flags are split: treat Regional Indicator pairs as a single glyph.

Second consecutive RI (U+1F1E6–U+1F1FF) should continue the current flag glyph; today it flushes, producing two glyphs (e.g., "πŸ‡ΊπŸ‡Έ").

Apply inside the Emoji.IsEmoji(...) branch:

-                    if (Emoji.IsEmoji(runes.Current.ToString()))
+                    if (Emoji.IsEmoji(runes.Current.ToString()))
                     {
-                        if (lastRune.Value == Codepoints.ZWJ ||
-                            lastRune.Value == Codepoints.ORC)
+                        // Regional Indicator pair: continue current glyph
+                        if (lastRune.Value >= 0x1F1E6 && lastRune.Value <= 0x1F1FF &&
+                            runes.Current.Value >= 0x1F1E6 && runes.Current.Value <= 0x1F1FF)
+                        {
+                            emoji.Append(runes.Current);
+                        }
+                        else if (lastRune.Value == Codepoints.ZWJ ||
+                                 lastRune.Value == Codepoints.ORC)
                         {
                             // the last char was a joiner or object replacement, so we continue building the emoji
                             emoji.Append(runes.Current);
                         }
                         else
                         {
                             // we have a new emoji starting, so we flush any existing emoji buffer
                             // ending the previous glyph and starting a new one
                             if (emoji.Length > 0)
                             {
                                 glyphs.Add(emoji.ToString());
                                 emoji.Clear();
                             }
 
                             emoji.Append(runes.Current);
                         }
                     }

Please add tests for flags: "πŸ‡ΊπŸ‡Έ", "πŸ‡©πŸ‡ͺ", text+flag mixtures; verify each is a single glyph and width 2.

🧹 Nitpick comments (1)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

106-118: Minor: use glyphs[^1] consistently for clarity.

Replace glyphs[glyphs.Count - 1] with glyphs[^1] to match existing usage above and reduce verbosity.

Example:

-else if (glyphs.Count > 0) glyphs[^1] = glyphs[^1] + runes.Current;
+else if (glyphs.Count > 0) glyphs[^1] = glyphs[^1] + runes.Current;
@@
-                            string lastGlyph = glyphs[glyphs.Count - 1];
-                            glyphs[glyphs.Count - 1] = lastGlyph + runes.Current;
+                            glyphs[^1] = glyphs[^1] + runes.Current;

Also applies to: 129-134

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between be6b19e and d020cd0.

πŸ“’ Files selected for processing (4)
  • src/Consolonia.Controls/ControlUtils.cs (1 hunks)
  • src/Consolonia.Core/Dummy/DummyConsole.cs (1 hunks)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs (1 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Consolonia.Core/Dummy/DummyConsole.cs
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Controls/ControlUtils.cs
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs
🧬 Code graph analysis (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (3)
  • WriteText (83-83)
  • SetCaretPosition (27-27)
  • PixelBufferCoordinate (33-33)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-94)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: inspection
  • GitHub Check: build
πŸ”‡ Additional comments (3)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)

7-7: LGTM!

The new using directive is required for the GetGlyphs extension method used in line 93.


24-29: LGTM!

The split from a single emoji support flag to separate complex emoji and variation support flags is well-designed and aligns with the broader capability surface changes across the codebase.


143-148: LGTM!

The emoji variation detection logic is sound: it writes a character with the emoji variation selector (U+FE0F) and checks whether the cursor advances by 2 positions, indicating proper terminal support for variation selectors.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (7)
src/Consolonia.Core/Text/TextShaper.cs (3)

15-39: Harden glyph caches: overflow guard and dedicated lock.

  • Casting GlyphByIndex.Count to ushort will overflow β‰₯ 65536 unique glyphs.
  • Locking on a mutable dictionary is brittle; use a private sync object shared by both maps.
  • Make GetGlyphByIndex lock consistently and give a clearer error.
@@
-        private static readonly Dictionary<ushort, string> GlyphByIndex = new();
-        private static readonly Dictionary<string, ushort> IndexByGlyph = new();
+        private static readonly object GlyphCacheSync = new();
+        private static readonly Dictionary<ushort, string> GlyphByIndex = new();
+        private static readonly Dictionary<string, ushort> IndexByGlyph = new();
@@
-                lock (IndexByGlyph)
+                lock (GlyphCacheSync)
                 {
                     if (!IndexByGlyph.TryGetValue(glyph, out glyphIndex))
                     {
-                        glyphIndex = (ushort)GlyphByIndex.Count;
+                        if (GlyphByIndex.Count >= ushort.MaxValue)
+                            throw new InvalidOperationException("Glyph cache exhausted (β‰₯ 65536 unique glyphs).");
+                        glyphIndex = (ushort)GlyphByIndex.Count;
                         GlyphByIndex[glyphIndex] = glyph;
                         IndexByGlyph[glyph] = glyphIndex;
                     }
                 }

Optional non-throwing API:

public static bool TryGetGlyphByIndex(ushort glyphIndex, out string glyph)
{
    lock (GlyphCacheSync) return GlyphByIndex.TryGetValue(glyphIndex, out glyph);
}

27-44: Use int loop index and explicit GlyphInfo argument types.

ushort i can wrap for large inputs; cast args to match Avalonia’s signature.

-            for (ushort i = 0; i < shapedBuffer.Length; i++)
+            for (int i = 0; i < shapedBuffer.Length; i++)
             {
                 string glyph = glyphs[i];
                 ushort glyphIndex;
-                lock (IndexByGlyph)
+                lock (IndexByGlyph)
                 {
                     if (!IndexByGlyph.TryGetValue(glyph, out glyphIndex))
                     {
                         glyphIndex = (ushort)GlyphByIndex.Count;
                         GlyphByIndex[glyphIndex] = glyph;
                         IndexByGlyph[glyph] = glyphIndex;
                     }
                 }
 
                 // NOTE: We are using the placeholder glyph since we are pushing
                 // raw text to the console and not using a font system to render the text
-                shapedBuffer[i] = new GlyphInfo(glyphIndex, i, glyph.MeasureText());
+                shapedBuffer[i] = new GlyphInfo(glyphIndex, i, (double)glyph.MeasureText());
             }

49-55: Lock the same sync as writers and fail clearly on miss.

-        public static string GetGlyphByIndex(ushort glyphIndex)
+        public static string GetGlyphByIndex(ushort glyphIndex)
         {
-            lock (IndexByGlyph)
+            lock (GlyphCacheSync)
             {
-                return GlyphByIndex[glyphIndex];
+                if (!GlyphByIndex.TryGetValue(glyphIndex, out var glyph))
+                    throw new KeyNotFoundException($"Glyph index {glyphIndex} not found in cache.");
+                return glyph;
             }
         }
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

83-118: Caret/head desync and over‑clear cause artifacts; advance bufferPoint and only clear trailing cell(s).

  • Variation branch doesn’t advance bufferPoint; final head uses stale _headBufferPoint.
  • Wide-glyph path clears glyphWidth spaces; should clear glyphWidth - 1 at X+1.
             ushort textWidth = str.MeasureText();
@@
-            // move to position
+            // write at requested position
             if (SupportsEmojiVariation)
             {
                 SetCaretPosition(bufferPoint);
                 WriteText(str);
+                bufferPoint = new PixelBufferCoordinate((ushort)(bufferPoint.X + textWidth), bufferPoint.Y);
             }
             else
             {
                 // rendering over the top with the glyph.
                 // process each glyph, rendering the width as spaces then moving the cursor and
                 foreach (string glyph in str.GetGlyphs(SupportsComplexEmoji))
                 {
                     ushort glyphWidth = glyph.MeasureText();
                     if (glyphWidth > 1)
                     {
-                        WriteText(Esc.SetCursorPosition(bufferPoint.X, bufferPoint.Y));
-                        WriteText(new string(' ', (int)Math.Min(Size.Width - bufferPoint.X, bufferPoint.X + glyphWidth)));
+                        // clear only trailing cell(s) of this wide glyph
+                        WriteText(Esc.SetCursorPosition(bufferPoint.X + 1, bufferPoint.Y));
+                        WriteText(new string(' ', Math.Min(glyphWidth - 1, Size.Width - (bufferPoint.X + 1))));
                     }
 
                     WriteText(Esc.SetCursorPosition(bufferPoint.X, bufferPoint.Y));
                     WriteText(glyph);
 
                     bufferPoint =
                         new PixelBufferCoordinate((ushort)(bufferPoint.X + glyphWidth), bufferPoint.Y);
                 }
             }
 
             WriteText(Esc.Reset);
-            if (_headBufferPoint.X < Size.Width - textWidth)
-                _headBufferPoint =
-                    new PixelBufferCoordinate((ushort)(_headBufferPoint.X + textWidth), _headBufferPoint.Y);
-            else
-                _headBufferPoint = (PixelBufferCoordinate)((ushort)0, (ushort)(_headBufferPoint.Y + 1));
+            _headBufferPoint = bufferPoint;
src/Consolonia.Controls/ControlUtils.cs (2)

60-73: Gate FE0E/FE0F width changes to valid bases (emoji or keycap).

Currently β€œx\uFE0F” becomes width 2. Limit variation-based width flips to emoji or keycap bases; track last rune.

@@
-            ushort width = 0;
-            ushort lastWidth = 0;
+            ushort width = 0;
+            ushort lastWidth = 0;
+            var lastRune = new Rune();
@@
-                    else if (rune.Value == Codepoints.VariationSelectors.EmojiSymbol &&
-                             lastWidth == 1)
+                    else if (rune.Value == Codepoints.VariationSelectors.EmojiSymbol &&
+                             lastWidth == 1 &&
+                             (Emoji.IsEmoji(lastRune.ToString()) || IsKeycapBase(lastRune.Value)))
                     {
                         // adjust for the emoji presentation, which is width 2
                         width++;
                         lastWidth = 2;
                     }
-                    else if (rune.Value == Codepoints.VariationSelectors.TextSymbol &&
-                             lastWidth == 2)
+                    else if (rune.Value == Codepoints.VariationSelectors.TextSymbol &&
+                             lastWidth == 2 &&
+                             (Emoji.IsEmoji(lastRune.ToString()) || IsKeycapBase(lastRune.Value)))
                     {
                         // adjust for the text presentation, which is width 1
                         width--;
                         lastWidth = 1;
                     }
@@
-                    if (runeWidth > 0) lastWidth = (ushort)runeWidth;
+                    if (runeWidth > 0) lastWidth = (ushort)runeWidth;
+                    // update lastRune at end of iteration
+                    lastRune = rune;
                 }
@@
             }
 
+            static bool IsKeycapBase(int v) => (v >= '0' && v <= '9') || v == '#' || v == '*';

82-95: Fix Regional Indicator (RI) accounting: handle odd singles and reset on exit.

Odd/lone RI is dropped; RI parity leaks across non‑RI. Add reset when leaving RI and account pending odd at EoS.

@@
-                    else if (rune.Value >= 0x1F1E6 && rune.Value <= 0x1F1FF)
+                    else if (rune.Value >= 0x1F1E6 && rune.Value <= 0x1F1FF)
                     {
                         regionalRuneCount++;
                         if (regionalRuneCount % 2 == 0)
                             // every pair of regional indicator symbols form a single glyph
                             width += (ushort)runeWidth;
                         // If the last rune is a regional indicator symbol, continue the current glyph
                     }
                     else
                     {
+                        // leaving RI run: if we had a pending single RI, count it now
+                        if ((regionalRuneCount % 2) == 1)
+                        {
+                            // RI width is 2 as emoji; fall back to Unicode width if needed
+                            int pending = Emoji.IsEmoji("πŸ‡¦") ? 2 : runeWidth; // placeholder; RI is wide on our path
+                            if (pending > 0) width += (ushort)pending;
+                            regionalRuneCount = 0;
+                        }
                         width += (ushort)runeWidth;
+                        regionalRuneCount = 0;
                     }
@@
-            return width;
+            // End-of-string: pending single RI
+            if ((regionalRuneCount % 2) == 1)
+            {
+                // Use RI width as 2 (emoji presentation)
+                width += 2;
+            }
+            return width;

Also applies to: 111-111

src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

166-175: Reset RI state when leaving RI sequences.

Parity leaks across plain text. Reset here to avoid pairing across non‑adjacent runs.

                 else
                 {
                     if (emoji.Length > 0)
                     {
                         glyphs.Add(emoji.ToString());
                         emoji.Clear();
                     }
-
+                    regionalRuneCount = 0;
                     glyphs.Add(runes.Current.ToString());
                 }
🧹 Nitpick comments (1)
src/Consolonia.Controls/ControlUtils.cs (1)

45-45: Avoid per‑rune string allocs in hot path.

Emoji.IsEmoji(rune.ToString()) allocates each iteration. Consider a cached set/range check or a span-friendly helper to cut allocations.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between c68b367 and cd33934.

πŸ“’ Files selected for processing (4)
  • src/Consolonia.Controls/ControlUtils.cs (1 hunks)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs (1 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
  • src/Consolonia.Core/Text/TextShaper.cs (3 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Text/TextShaper.cs
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs
  • src/Consolonia.Controls/ControlUtils.cs
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
🧬 Code graph analysis (2)
src/Consolonia.Core/Text/TextShaper.cs (1)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-112)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (3)
  • WriteText (83-83)
  • SetCaretPosition (27-27)
  • PixelBufferCoordinate (33-33)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-112)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
πŸ”‡ Additional comments (5)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (2)

127-142: Variation selectors: good handling.

Appending FE0E/FE0F to in‑progress or last glyph looks correct and matches shaping expectations.


145-164: supportsComplexEmoji gating grouped correctly.

Parentheses avoid precedence bugs; continuing emoji after ZWJ/ORC only when supported is correct.

src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)

145-151: Variation detection: OK, but keep screen clean.

Detection is fine; ClearScreen follows, so no action required.


59-79: Attribute write before positioning: acceptable.

Attributes are global; writing them before SetCursorPosition is fine.


59-118: Verify cursor position tracking and rendering on Linux console and gallery/TurboVision theme after applying fixes.

The code shows proper cursor position synchronization through SetCaretPosition() (line 45) which updates _headBufferPoint, and both rendering paths handle cursor tracking. For the SupportsEmojiVariation branch, SetCaretPosition() is called to sync state. The complex emoji branch manually tracks bufferPoint during glyph iteration, with _headBufferPoint updated at the method end (lines 113–117). ClearScreen() properly resets the position (line 203). Retest on Linux to confirm the head synchronization and screen clearing work correctly across environments and themes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

112-117: Simplify head tracking after fixing bufferPoint advancement.

Once bufferPoint is correctly advanced in both rendering paths (see previous comments), this complex wrap logic should be simplified to _headBufferPoint = bufferPoint; as recommended in past reviews.

Additionally, the line-wrap logic (lines 116-117) appears questionable: why would Print need to wrap the head position to the next line? The caller (e.g., PixelBuffer.PrintBuffer) typically manages line breaks explicitly. Consider whether this wrap logic is necessary or if it's masking an upstream issue.

After fixing the bufferPoint advancement in the SupportsEmojiVariation=true branch, apply:

 WriteText(Esc.Reset);
-if (_headBufferPoint.X < Size.Width - textWidth)
-    _headBufferPoint =
-        new PixelBufferCoordinate((ushort)(_headBufferPoint.X + textWidth), _headBufferPoint.Y);
-else
-    _headBufferPoint = (PixelBufferCoordinate)((ushort)0, (ushort)(_headBufferPoint.Y + 1));
+_headBufferPoint = bufferPoint;
♻️ Duplicate comments (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

86-90: Critical: bufferPoint must be advanced after writing text.

The SupportsEmojiVariation=true path writes the string but never advances bufferPoint. This causes _headBufferPoint tracking to break because the final update logic (lines 113-117) relies on either an advanced bufferPoint or the previous _headBufferPoint state, leading to incorrect cursor positioning after this method returns.

Apply this diff to advance bufferPoint after writing:

 if (SupportsEmojiVariation)
 {
     SetCaretPosition(bufferPoint);
     WriteText(str);
+    bufferPoint = new PixelBufferCoordinate((ushort)(bufferPoint.X + textWidth), bufferPoint.Y);
 }
🧹 Nitpick comments (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

147-150: Consider testing variation selector handling more explicitly.

The current test writes "πŸ—‘" and checks if the cursor advances by 2 positions. However, to truly verify variation selector support, consider testing a character that changes width based on the variation selector. For example:

-// write out a char with wide variation selector
-WriteText("πŸ—‘");
+// Test variation selector: ☺ defaults to text (width 1), ☺️ (with FE0F) is emoji (width 2)
+WriteText("☺\uFE0F");
 (int left3, _) = Console.GetCursorPosition();
 _supportsEmojiVariation = left3 - left2 == 2;

This would more accurately detect whether the terminal respects variation selectors to control text vs. emoji presentation.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 2c700d0 and 9ddc62e.

πŸ“’ Files selected for processing (1)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
🧬 Code graph analysis (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (3)
  • WriteText (83-83)
  • SetCaretPosition (27-27)
  • PixelBufferCoordinate (33-33)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
πŸ”‡ Additional comments (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

24-29: LGTM: Clean capability flag implementation.

The emoji capability tracking fields and properties are well-structured and follow a consistent pattern.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 9ddc62e and ce027f3.

πŸ“’ Files selected for processing (1)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
🧬 Code graph analysis (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (3)
  • WriteText (83-83)
  • SetCaretPosition (27-27)
  • PixelBufferCoordinate (33-33)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
πŸ”‡ Additional comments (2)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (2)

7-7: LGTM: Import and emoji capability fields added.

The new import and fields correctly support the enhanced emoji detection and rendering logic.

Also applies to: 24-29


145-150: LGTM: Emoji capability detection logic is sound.

The detection approach is reasonable:

  • Complex emoji support is detected by rendering a ZWJ sequence and checking if the cursor advances by exactly 2 positions
  • Emoji variation support is detected by rendering a wide emoji character and verifying the cursor movement

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/Consolonia.Core/Text/TextShaper.cs (4)

43-43: Type clarity: Add explicit casts for GlyphInfo constructor.

According to previous analysis, GlyphInfo expects (ushort GlyphIndex, int GlyphCluster, double GlyphAdvance), but the call passes ushort for all three arguments, relying on implicit conversions. While valid, explicit casts improve clarity and intent.

Apply this diff for explicit type conversions:

-                shapedBuffer[i] = new GlyphInfo(glyphIndex, i, glyph.MeasureText());
+                shapedBuffer[i] = new GlyphInfo(glyphIndex, (int)i, (double)glyph.MeasureText());

Note: If you fix the loop index to int (line 27), then only the third argument needs casting:

-                shapedBuffer[i] = new GlyphInfo(glyphIndex, i, (double)glyph.MeasureText());
+                shapedBuffer[i] = new GlyphInfo(glyphIndex, i, (double)glyph.MeasureText());

49-55: Improve error handling in GetGlyphByIndex.

The method now uses the correct lock (GlyphCacheSync), but still uses direct dictionary indexing that will throw KeyNotFoundException without context. Previous review suggested using TryGetValue with a clearer error message or adding a non-throwing TryGetGlyphByIndex API.

Apply this diff to improve error handling:

         public static string GetGlyphByIndex(ushort glyphIndex)
         {
             lock (GlyphCacheSync)
             {
-                return GlyphByIndex[glyphIndex];
+                if (!GlyphByIndex.TryGetValue(glyphIndex, out var glyph))
+                    throw new KeyNotFoundException($"Glyph index {glyphIndex} not found in cache.");
+                return glyph;
             }
         }

Optionally, add a non-throwing API for safer access:

public static bool TryGetGlyphByIndex(ushort glyphIndex, out string glyph)
{
    lock (GlyphCacheSync) 
        return GlyphByIndex.TryGetValue(glyphIndex, out glyph);
}

27-28: Critical: ushort loop index will wrap and hang for long inputs.

The loop index i declared as ushort will wrap at 65535 and spin indefinitely when shapedBuffer.Length >= 65536. This was flagged previously but remains unfixed.

Apply this diff to fix:

-            for (ushort i = 0; i < shapedBuffer.Length; i++)
+            for (int i = 0; i < shapedBuffer.Length; i++)

35-38: Critical: Guard against cache overflow.

Casting GlyphByIndex.Count to ushort without checking will overflow at 65536 unique glyphs, corrupting the cache mappings. This was previously flagged as needing an overflow guard but remains unfixed.

Apply this diff to add an overflow check:

                     if (!IndexByGlyph.TryGetValue(glyph, out glyphIndex))
                     {
+                        if (GlyphByIndex.Count >= ushort.MaxValue)
+                            throw new InvalidOperationException("Glyph cache exhausted (β‰₯ 65536 unique glyphs).");
                         glyphIndex = (ushort)GlyphByIndex.Count;
                         GlyphByIndex[glyphIndex] = glyph;
                         IndexByGlyph[glyph] = glyphIndex;
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3d2e35a and 181ad73.

πŸ“’ Files selected for processing (2)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
  • src/Consolonia.Core/Text/TextShaper.cs (2 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Text/TextShaper.cs
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
🧬 Code graph analysis (2)
src/Consolonia.Core/Text/TextShaper.cs (1)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (3)
  • WriteText (83-83)
  • SetCaretPosition (27-27)
  • PixelBufferCoordinate (33-33)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: inspection
πŸ”‡ Additional comments (4)
src/Consolonia.Core/Text/TextShaper.cs (1)

14-16: Dedicated lock object properly implemented.

The dedicated GlyphCacheSync object correctly addresses the previous concern about locking on mutable dictionaries. Good fix!

src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)

24-29: LGTM: Clean separation of emoji support flags.

The nullable fields with public properties providing safe defaults (false when uninitialized) is a good pattern. The separation of SupportsComplexEmoji (composite surrogate pairs) and SupportsEmojiVariation (cursor advancement for wide chars) allows granular terminal capability detection.


86-114: LGTM: Rendering paths correctly handle terminal capabilities.

The dual-path approach properly handles terminals with and without automatic cursor advancement for wide characters:

  • SupportsEmojiVariation=true (lines 86-91): Relies on terminal to advance cursor correctly, then updates tracking.
  • SupportsEmojiVariation=false (lines 93-111): Manually renders spaces for wide glyphs to ensure proper display and cursor positioning.

The caret tracking desync, space calculation bugs, and other critical issues flagged in previous reviews have been properly addressedβ€”particularly line 114's _headBufferPoint = bufferPoint which ensures consistent head tracking across both paths.


7-7: LGTM: Required import for glyph enumeration.

The import of Consolonia.Core.Helpers is needed for the GetGlyphs extension method used in the per-glyph rendering path (line 96).

@tomlm
Copy link
Collaborator Author

tomlm commented Oct 23, 2025

When I switch to TextBlock in gallery in TurboVision theme, listbox item dissapears. image Then it needs redrawing/mouse over to appear again.

This is fixed

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

94-95: Polish the comment for clarity.

The comment ends abruptly with "and", making it feel incomplete. Consider revising for better readability.

Apply this diff to clarify the comment:

-                // rendering over the top with the glyph.
-                // process each glyph, rendering the width as spaces then moving the cursor and
+                // Process each glyph individually: for wide glyphs, clear trailing cells with spaces
+                // to prevent artifacts, then position the cursor and render the glyph.
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 181ad73 and 3e4f968.

πŸ“’ Files selected for processing (2)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs (2 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cs
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
🧬 Code graph analysis (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: inspection
  • GitHub Check: build
πŸ”‡ Additional comments (2)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (2)

86-114: Excellent refactoringβ€”both rendering paths are now correct.

The logic correctly handles terminals with and without emoji variation support:

  • SupportsEmojiVariation=true: Simple path writes the full string and advances the cursor.
  • SupportsEmojiVariation=false: Per-glyph rendering clears trailing cells for wide glyphs to prevent artifacts, then positions and renders each glyph individually.

Head position tracking is now synchronized properly in both branches (line 114), and the space-clearing calculation correctly accounts for available width (Size.Width - bufferPoint.X - 1) after positioning the cursor at bufferPoint.X + 1.


142-147: Emoji variation detection implemented correctly.

The detection logic properly tests terminal support by:

  1. Writing a complex emoji and checking cursor advancement for _supportsComplexEmoji (line 142).
  2. Writing a wastebasket emoji with explicit U+FE0F emoji variation selector and checking cursor advancement for _supportsEmojiVariation (lines 144-147).

The explicit variation selector on line 145 ensures deterministic behavior across terminals, addressing the earlier review feedback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

85-85: Optional: Clarify comments for maintainability.

The comment on line 85 ("move to position") describes only the SupportsEmojiVariation branch, not the overall structure. The comment on lines 94-95 ends mid-sentence ("moving the cursor and"). While the code is clear, more precise comments would aid future maintenance.

Consider:

-            // move to position
+            // Render text using terminal's native emoji support
             if (SupportsEmojiVariation)
-                // rendering over the top with the glyph.
-                // process each glyph, rendering the width as spaces then moving the cursor and
+                // Fallback: manually render each glyph with explicit space-clearing for wide characters
+                // to work around terminals that don't advance cursor correctly for emoji

Also applies to: 94-95

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3e4f968 and 55a9ca0.

πŸ“’ Files selected for processing (1)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
🧬 Code graph analysis (1)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: inspection
πŸ”‡ Additional comments (3)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)

7-7: LGTM: Clean separation of emoji support detection.

The new fields and properties properly encapsulate the two detection modes (complex emoji and emoji variation), with sensible defaults.

Also applies to: 24-29


80-114: LGTM: Dual-path rendering correctly handles both terminal capabilities.

The fast path (SupportsEmojiVariation) and fallback path (per-glyph) both correctly track bufferPoint and update _headBufferPoint at the end. The space-clearing logic for wide glyphs (lines 99-103) is mathematically sound: it positions at bufferPoint.X + 1 and writes Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1) spaces to clear trailing cells.


137-147: LGTM: Emoji detection logic is sound.

Both detection tests correctly measure cursor movement after writing test strings. Line 145 properly includes the emoji variation selector \uFE0F, ensuring reliable detection of terminals that honor variation selectors.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (7)
src/Tools/Sandbox/MainWindow.axaml (1)

6-7: Consider adding a comment to document the test case.

These controls appear to test emoji rendering with variation selectors (the wastebasket emoji πŸ—‘ in a TextBox and bound TextBlock). Adding a brief comment would help future developers understand this is an intentional test case for the variation selector functionality.

Optionally, a more descriptive name than "foo" (e.g., "emojiTestInput") would improve clarity.

src/Consolonia.Core/Text/ConsoleTypeface.cs (3)

94-105: Avoid nested re-entrant locking in GetGlyphAdvances

You lock, then call GetGlyphAdvance (which locks again). Read WidthByIndex directly under one lock.

-        public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
-        {
-            int[] advances = new int[glyphs.Length];
-            lock (GlyphCacheSync)
-            {
-                for (int i = 0; i < glyphs.Length; i++)
-                {
-                    advances[i] = GetGlyphAdvance(glyphs[i]);
-                }
-            }
-            return advances;
-        }
+        public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
+        {
+            int[] advances = new int[glyphs.Length];
+            lock (GlyphCacheSync)
+            {
+                for (int i = 0; i < glyphs.Length; i++)
+                {
+                    advances[i] = WidthByIndex[glyphs[i]];
+                }
+            }
+            return advances;
+        }

14-18: Unbounded static caches

These grow process-wide and never clear. Consider a capacity cap/LRU or a Clear() hook when the console shuts down.


9-12: Typo in doc comment

β€œpsuedo-typeface” β†’ β€œpseudo-typeface”.

-    /// This represents a psuedo-typeface for console rendering.
+    /// This represents a pseudo-typeface for console rendering.
src/Consolonia.Core/Text/FontManagerImpl.cs (3)

36-40: Creation of ConsoleTypeface ignores family/stretch

Good switch. Consider rejecting unsupported families or honoring stretch for completeness.

-            glyphTypeface = new ConsoleTypeface
+            if (!string.Equals(familyName, GetTheOnlyFontFamilyName(), StringComparison.Ordinal))
+                return false;
+            glyphTypeface = new ConsoleTypeface
             {
                 Weight = weight,
                 Style = style
             };

49-49: Stream + FontSimulations overload ignores simulations

If Avalonia passes Bold/Italic simulations, consider mapping to Weight/Style or documenting that simulations are no-op.


57-57: Stream overload parity

Same as above; consider accepting desired Weight/Style via parameters or defaulting consistently with string overload.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 55a9ca0 and 9927ed6.

πŸ“’ Files selected for processing (6)
  • src/Consolonia.Core/Drawing/DrawingContextImpl.cs (3 hunks)
  • src/Consolonia.Core/Text/ConsoleTypeface.cs (1 hunks)
  • src/Consolonia.Core/Text/FontManagerImpl.cs (2 hunks)
  • src/Consolonia.Core/Text/GlyphTypeface.cs (0 hunks)
  • src/Consolonia.Core/Text/TextShaper.cs (3 hunks)
  • src/Tools/Sandbox/MainWindow.axaml (1 hunks)
πŸ’€ Files with no reviewable changes (1)
  • src/Consolonia.Core/Text/GlyphTypeface.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Consolonia.Core/Text/TextShaper.cs
🧰 Additional context used
πŸ““ Path-based instructions (1)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Text/FontManagerImpl.cs
  • src/Consolonia.Core/Drawing/DrawingContextImpl.cs
  • src/Consolonia.Core/Text/ConsoleTypeface.cs
🧬 Code graph analysis (3)
src/Consolonia.Core/Text/FontManagerImpl.cs (1)
src/Consolonia.Core/Text/ConsoleTypeface.cs (1)
  • ConsoleTypeface (12-133)
src/Consolonia.Core/Drawing/DrawingContextImpl.cs (5)
src/Consolonia.Core/Text/TextShaper.cs (1)
  • TextShaper (15-42)
src/Consolonia.Core/Infrastructure/Regions.cs (1)
  • AddRect (50-72)
src/Consolonia.Core/Text/ConsoleTypeface.cs (2)
  • ConsoleTypeface (12-133)
  • GetGlyphText (68-74)
src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs (2)
  • ConsoloniaPlatform (20-227)
  • RaiseNotSupported (100-103)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)
  • Symbol (172-183)
src/Consolonia.Core/Text/ConsoleTypeface.cs (3)
src/Consolonia.Core/Drawing/DrawingContextImpl.cs (2)
  • DrawingContextImpl (18-1142)
  • DrawingContextImpl (63-68)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Text/FontManagerImpl.cs (2)
  • FontManagerImpl (13-65)
  • GetTheOnlyFontFamilyName (61-64)
πŸ”‡ Additional comments (3)
src/Consolonia.Core/Drawing/DrawingContextImpl.cs (1)

279-294: Per-glyph rendering loop looks correct.

The refactor from string-based to per-glyph rendering is well-structured:

  • Position advancement correctly uses glyphInfo.GlyphAdvance to account for wide characters.
  • Dirty region calculation properly spans from start to final glyph position.
  • The height calculation position.Y - startPosition.Y + 1 correctly handles single-line and multi-line scenarios.
src/Consolonia.Core/Text/ConsoleTypeface.cs (2)

118-130: Verify FontMetrics signs/values

Ascent = -1 with DesignEmHeight = 1 is unusual; underline/strikethrough positions -1 may affect decoration placement. Consider Ascent = 1, Descent = 0 if metrics are 1-em normalized; adjust if tests rely on current values.

-            DesignEmHeight = 1,
-            Ascent = -1,
-            Descent = 0,
+            DesignEmHeight = 1,
+            Ascent = 1,
+            Descent = 0,
             LineGap = 0,
-            UnderlinePosition = -1,
+            UnderlinePosition = 0,
             UnderlineThickness = DrawingContextImpl.UnderlineThickness,
-            StrikethroughPosition = -1,
+            StrikethroughPosition = 0,
             StrikethroughThickness = DrawingContextImpl.StrikethroughThickness,

Please validate text baseline/decoration rendering on Linux/Windows consoles after any change.


76-85: Remove or replace unused GetGlyphs(ReadOnlySpan) method

The reviewed method at lines 76–85 is never called in the codebase. Text shaping uses the string-based GetGlyphs extension method (from UtilityExtensions.cs), which properly clusters emoji and variation sequences. The active flow in TextShaper.cs then calls GetGlyphIndex(string) on each clustered glyph, preserving widths correctly. Either delete this unused method or confirm it's retained for API compatibility.

Comment on lines +861 to 895
private void DrawGlyphInfo(IBrush foreground, GlyphInfo glyphInfo, IGlyphTypeface typeface, PixelPoint position)
{
var glyphTypeface = (ConsoleTypeface)typeface;
string glyph = glyphTypeface.GetGlyphText(glyphInfo.GlyphIndex);

if (foreground is not ISolidColorBrush solidColorBrush)
{
solidColorBrush = ConsoloniaPlatform.RaiseNotSupported<ISolidColorBrush>(
NotSupportedRequestCode.DrawStringWithNonSolidColorBrush, this, foreground, text, typeface, origin);
NotSupportedRequestCode.DrawStringWithNonSolidColorBrush, this, foreground, glyph, typeface,
position);

if (solidColorBrush == null)
return;
}

// if (!Transform.IsTranslateOnly()) ConsoloniaPlatform.RaiseNotSupported(15); //todo: what to do if a rotation?

var position = origin.Transform(Transform).ToPixelPoint();
PixelPoint startPosition = position;

// Each glyph maps to a pixel as a starting point.
// Emoji's and Ligatures are complex strings, so they start at a point and then overlap following pixels
// the x and y are adjusted accordingly.
foreach (string glyph in text.GetGlyphs(_consoleWindowImpl.Console.SupportsComplexEmoji))
Color foregroundColor = solidColorBrush.Color;

var symbol = new Symbol(glyph, (byte)glyphInfo.GlyphAdvance);
// if we are attempting to draw a wide glyph we need to make sure that the clipping point
// is for the last physical char. Aka a double char should be clipped if it's second rendered
// char would break the boundary of the clip.
if (CurrentClip.ContainsExclusive(position) &&
(symbol.Width == 1 ||
symbol.Width > 1 &&
CurrentClip.ContainsExclusive(new PixelPoint(position.X + symbol.Width - 1, position.Y))))
{
Color foregroundColor = solidColorBrush.Color;

switch (glyph)
{
case "\t":
position = position.WithX(position.X + glyph.MeasureText());
break;
case "\r":
case "\f":
case "\n":
position = new PixelPoint(startPosition.X, position.Y + 1);
break;
default:
{
var symbol = new Symbol(glyph);
// if we are attempting to draw a wide glyph we need to make sure that the clipping point
// is for the last physical char. Aka a double char should be clipped if it's second rendered
// char would break the boundary of the clip.
if (CurrentClip.ContainsExclusive(position) &&
(symbol.Width == 1 ||
symbol.Width > 1 &&
CurrentClip.ContainsExclusive(new PixelPoint(position.X + symbol.Width - 1, position.Y))))
{
var newPixel = new Pixel(symbol, foregroundColor, typeface.Style, typeface.Weight);
_pixelBuffer[position] = _pixelBuffer[position].Blend(newPixel);
}

position = position.WithX(position.X + symbol.Width);
}
break;
}
var newPixel = new Pixel(symbol, foregroundColor, typeface.Style, typeface.Weight);
_pixelBuffer[position] = _pixelBuffer[position].Blend(newPixel);
}

// mark the dirty region, start to end, position is after the last drawn char so
// already aligned on x; y we need to add 1 to give the rect height.
var rectToRefresh = new PixelRect(startPosition,
new PixelSize(position.X - startPosition.X,
position.Y - startPosition.Y + 1));
PixelRect intersectRect = CurrentClip.Intersect(rectToRefresh);
_consoleWindowImpl.DirtyRegions.AddRect(intersectRect);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Continuation cells for wide glyphs must be explicitly managed to prevent visual artifacts.

DrawGlyphInfo writes the head cell of wide glyphs (line 893) but does not mark or clear the continuation cells that wide characters occupy. For a glyph with symbol.Width > 1, the cells at position.X + 1 through position.X + symbol.Width - 1 are not explicitly handled.

This can cause stale buffer content in continuation cells to persist as visual artifacts, which matches the Linux console artifacts reported in the PR comments (page change artifacts, misalignment issues).

Reference the pattern in RenderTarget.FlushingBuffer (lines 177-181 in RenderTarget.cs per codebase search), which loops to fill trailing cells after writing the head cell:

for (int x = pixel.Position.X + 1; x < pixel.Position.X + pixel.Width; x++)
    // Fill with Pixel.Empty or continuation marker

Apply this fix after line 893 to explicitly mark continuation cells:

                 var newPixel = new Pixel(symbol, foregroundColor, typeface.Style, typeface.Weight);
                 _pixelBuffer[position] = _pixelBuffer[position].Blend(newPixel);
+                
+                // Mark continuation cells for wide glyphs to prevent stale content artifacts
+                for (int i = 1; i < symbol.Width; i++)
+                {
+                    var continuationPosition = new PixelPoint(position.X + i, position.Y);
+                    if (CurrentClip.ContainsExclusive(continuationPosition))
+                    {
+                        // Mark as continuation cell (adjust based on Pixel/Symbol continuation mechanism)
+                        _pixelBuffer[continuationPosition] = new Pixel(); // or appropriate continuation marker
+                    }
+                }
             }

Note: Verify the appropriate continuation cell marker for your Pixel/Symbol implementation (e.g., Pixel.Empty, new Pixel(), or a width-zero marker). Based on PixelBuffer.PrintBuffer (line 92: if (pixel.Width > 0)), continuation cells may need Width == 0.

πŸ€– Prompt for AI Agents
In src/Consolonia.Core/Drawing/DrawingContextImpl.cs around lines 861-895,
DrawGlyphInfo writes only the head cell for wide glyphs and does not mark/clear
the continuation cells (position.X+1 .. position.X+symbol.Width-1), causing
stale visual artifacts; after the head cell write (after line ~893) add a loop
that iterates x from position.X+1 to position.X+symbol.Width-1 and explicitly
sets each _pixelBuffer[new PixelPoint(x, position.Y)] to the proper continuation
marker (e.g., Pixel.Empty or a Pixel with Width==0/continuation semantics used
elsewhere) so trailing cells are cleared/marked consistently with
RenderTarget.FlushingBuffer and PixelBuffer.PrintBuffer expectations.

Comment on lines +23 to +37
public bool TryGetGlyphMetrics(ushort glyphIndex, out GlyphMetrics metrics)
{
lock (GlyphCacheSync)
{
var glyph = GlyphByIndex[glyphIndex];
metrics = new GlyphMetrics
{
XBearing = 0,
YBearing = 0,
Height = 1,
Width = WidthByIndex[glyphIndex],
};
return true;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Make TryGetGlyphMetrics truly β€œtry”; avoid KeyNotFoundException

Use TryGetValue and return false when glyphIndex is unknown; remove unused variable. Prevents crashes on unexpected indices.

-        public bool TryGetGlyphMetrics(ushort glyphIndex, out GlyphMetrics metrics)
-        {
-            lock (GlyphCacheSync)
-            {
-                var glyph = GlyphByIndex[glyphIndex];
-                metrics = new GlyphMetrics
-                {
-                    XBearing = 0,
-                    YBearing = 0,
-                    Height = 1,
-                    Width = WidthByIndex[glyphIndex],
-                };
-                return true;
-            }
-        }
+        public bool TryGetGlyphMetrics(ushort glyphIndex, out GlyphMetrics metrics)
+        {
+            lock (GlyphCacheSync)
+            {
+                if (!WidthByIndex.TryGetValue(glyphIndex, out var width) ||
+                    !GlyphByIndex.ContainsKey(glyphIndex))
+                {
+                    metrics = default;
+                    return false;
+                }
+                metrics = new GlyphMetrics
+                {
+                    XBearing = 0,
+                    YBearing = 0,
+                    Height = 1,
+                    Width = width,
+                };
+                return true;
+            }
+        }
πŸ€– Prompt for AI Agents
In src/Consolonia.Core/Text/ConsoleTypeface.cs around lines 23 to 37, the method
currently indexes into GlyphByIndex directly which can throw
KeyNotFoundException; change it to use GlyphByIndex.TryGetValue(glyphIndex, out
var glyphEntry) inside the existing lock, remove the unused local "glyph"
variable, and if TryGetValue returns false set metrics = default and return
false; if true populate metrics (use WidthByIndex[glyphIndex] only after
confirming presence or store width from the found entry) and return true.

Comment on lines +55 to +63
if (!IndexByGlyph.TryGetValue(glyph, out glyphIndex))
{
if (IndexByGlyph.Count >= ushort.MaxValue)
throw new InvalidOperationException("Glyph cache overflow.");
glyphIndex = (ushort)GlyphByIndex.Count;
GlyphByIndex[glyphIndex] = glyph;
WidthByIndex[glyphIndex] = glyph.MeasureText();
IndexByGlyph[glyph] = glyphIndex;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Fix off-by-one in glyph cache overflow check

The current guard blocks allocating the last valid index (65535). Allow it; only block when Count already equals 65536 post-add potential.

-                if (!IndexByGlyph.TryGetValue(glyph, out glyphIndex))
-                {
-                    if (IndexByGlyph.Count >= ushort.MaxValue)
-                        throw new InvalidOperationException("Glyph cache overflow.");
-                    glyphIndex = (ushort)GlyphByIndex.Count;
+                if (!IndexByGlyph.TryGetValue(glyph, out glyphIndex))
+                {
+                    if (GlyphByIndex.Count == ushort.MaxValue)
+                        throw new InvalidOperationException("Glyph cache overflow.");
+                    glyphIndex = (ushort)GlyphByIndex.Count;
                     GlyphByIndex[glyphIndex] = glyph;
                     WidthByIndex[glyphIndex] = glyph.MeasureText();
                     IndexByGlyph[glyph] = glyphIndex;
                 }

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In src/Consolonia.Core/Text/ConsoleTypeface.cs around lines 55 to 63, the
overflow guard currently prevents allocating the final valid ushort index
(65535). Change the condition so it only throws when the cache already contains
65536 entries β€” e.g., replace the check with a test that fires when
IndexByGlyph.Count > ushort.MaxValue (or IndexByGlyph.Count >= ushort.MaxValue +
1) so the last valid index can be allocated.

Comment on lines +107 to +111
public bool TryGetTable(uint tag, out byte[] table)
{
throw new NotImplementedException();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

Don’t throw from TryGetTable; return false

Throwing here can crash callers probing for optional tables.

-        public bool TryGetTable(uint tag, out byte[] table)
-        {
-            throw new NotImplementedException();
-        }
+        public bool TryGetTable(uint tag, out byte[] table)
+        {
+            table = Array.Empty<byte>();
+            return false;
+        }

Please confirm no Avalonia code path expects this to throw.


I'll now verify the review comment by examining the codebase for TryGetTable usage and callers.


🏁 Script executed:

#!/bin/bash
# Search for TryGetTable method definition and all usages
echo "=== TryGetTable Definition and Usage ==="
rg -nP '\bTryGetTable\b' --type=cs -C 5

echo ""
echo "=== Check if NotImplementedException is caught anywhere ==="
rg -nP 'catch.*NotImplementedException' --type=cs -C 3

echo ""
echo "=== Search for interface definitions that might define TryGetTable ==="
rg -nP 'interface.*Typeface|TryGetTable.*\{' --type=cs -C 3

Length of output: 991


🏁 Script executed:

#!/bin/bash
# Find the class declaration and what it implements
echo "=== ConsoleTypeface Class Declaration ==="
rg -nP '^[\s]*(?:public\s+)?class\s+ConsoleTypeface' --type=cs -A 5

echo ""
echo "=== Find all method calls to TryGetTable ==="
rg -nP '\.TryGetTable\s*\(' --type=cs -C 4

echo ""
echo "=== Search for interface that might contain TryGetTable ==="
rg -nP '^[\s]*(?:public\s+)?interface.*\{' --type=cs -A 15 | head -60

Length of output: 148


🏁 Script executed:

#!/bin/bash
# View the file to understand structure
echo "=== View ConsoleTypeface.cs file ==="
cat src/Consolonia.Core/Text/ConsoleTypeface.cs | head -150

echo ""
echo "=== Search for simple callers ==="
rg -n 'TryGetTable' --type=cs

Length of output: 4692


🏁 Script executed:

#!/bin/bash
# Find IGlyphTypeface interface definition
echo "=== IGlyphTypeface Interface Definition ==="
rg -n 'interface IGlyphTypeface' --type=cs -A 50

echo ""
echo "=== Check if Avalonia.Media is external (from NuGet) ==="
fd -e csproj | xargs grep -l 'Avalonia' 2>/dev/null | head -5

echo ""
echo "=== Check formatting compliance with editorconfig ==="
echo "Looking for .editorconfig file:"
fd -name '.editorconfig' -type f

Length of output: 668


🏁 Script executed:

#!/bin/bash
# Find .editorconfig file correctly
echo "=== Looking for .editorconfig ==="
find . -name '.editorconfig' -type f 2>/dev/null

echo ""
echo "=== View the .editorconfig for src/ ==="
if [ -f "src/.editorconfig" ]; then
  cat src/.editorconfig
fi

Length of output: 236076


🌐 Web query:

Avalonia IGlyphTypeface TryGetTable optional table font interface

πŸ’‘ Result:

Short answer: IGlyphTypeface.TryGetTable lets you request the raw OpenType/TrueType table data (by 4‑byte tag) β€” it returns a bool and fills a byte[] with the table contents if present. See the IGlyphTypeface API. [1]

Key points and example:

  • Signature (API): TryGetTable(UInt32 tag, Byte[] buffer) β€” returns true if table data was returned. [1]
  • The tag is the 4-byte OpenType/TrueType table tag (e.g., 'GSUB', 'GPOS', 'name', 'cmap') packed into a UInt32.
  • Typical packing (big-endian FourCC): uint tag = (uint)('G')<<24 | (uint)('S')<<16 | (uint)('U')<<8 | (uint)('B');
  • Usage pattern:
    • Obtain an IGlyphTypeface (e.g. via FontManager/IFontCollection TryGetGlyphTypeface). [2]
    • Call TryGetTable(tag, buffer). If it returns true the buffer contains the table bytes; if false the table is not present or unavailable.

Minimal C# example:
uint Tag(string s) => (uint)s[0]<<24 | (uint)s[1]<<16 | (uint)s[2]<<8 | (uint)s[3];
var tag = Tag("GSUB");
byte[] buf = new byte[/* expected/max size or call once to probe */];
if (glyphTypeface.TryGetTable(tag, buf))
{
// buf contains table bytes (size may be full length or partial depending on implementation)
}

References:

  • IGlyphTypeface (TryGetTable). [1]
  • FontManager / TryGetGlyphTypeface (obtaining IGlyphTypeface). [2]

[1] Avalonia API β€” IGlyphTypeface. [2] Avalonia API β€” FontManager.TryGetGlyphTypeface.


Return false instead of throwing to honor Try/Fail pattern for optional font tables

The TryGetTable method implements Avalonia's IGlyphTypeface interface, which follows the .NET Try/Fail convention where "Try" methods return bool and must never throwβ€”they indicate failure by returning false. Throwing NotImplementedException violates this contract and will crash callers that probe for optional OpenType/TrueType tables. Return false with an empty byte array instead.

        public bool TryGetTable(uint tag, out byte[] table)
        {
-            throw new NotImplementedException();
+            table = Array.Empty<byte>();
+            return false;
        }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public bool TryGetTable(uint tag, out byte[] table)
{
throw new NotImplementedException();
}
public bool TryGetTable(uint tag, out byte[] table)
{
table = Array.Empty<byte>();
return false;
}
πŸ€– Prompt for AI Agents
In src/Consolonia.Core/Text/ConsoleTypeface.cs around lines 107 to 111, the
TryGetTable method currently throws NotImplementedException which breaks the
Try/Fall pattern; change it to never throw: set the out parameter to an empty
byte array (Array.Empty<byte>()) and return false when the requested table is
not available, preserving the TryX contract for optional font tables.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/Consolonia.Core/Text/TextShaper.cs (1)

28-28: Critical: ushort loop index can wrap and cause infinite loop.

The loop index i is declared as ushort, which will wrap at 65,535. If shapedBuffer.Length >= 65,536, the loop condition will never become false, causing an infinite loop and hanging the application.

Apply this diff:

-            for (ushort i = 0; i < shapedBuffer.Length; i++)
+            for (int i = 0; i < shapedBuffer.Length; i++)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

99-103: Critical: break statement truncates remaining text.

When the terminal doesn't support complex emoji and a ZWJ/ORC is encountered, the break statement at line 102 exits the entire while loop, truncating all remaining text in the input string. This causes text after the joiner to be silently dropped.

The past review comment recommended using continue instead, but that's also problematic because it would leave the joiner in lastRune. Better to skip the joiner without updating lastRune:

                     else
                     {
-                        // we terminate multi-chained 1 because terminal doesn't support it 
-                        break;
+                        // terminal doesn't support chaining; skip the joiner
+                        // don't update lastRune or index so it's as if the joiner wasn't there
                     }

Note: You'll need to move the lastRune and index updates inside the if statement or restructure the logic to avoid updating them for skipped joiners.

src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2)

124-141: Critical: "x" does not support text/emoji variation selectors.

The test case includes "x" (line 125) as input for text presentation testing. However, "x" is a basic Latin letter that does not have emoji/text presentation variants. Adding U+FE0E to "x" is meaningless, and terminals will handle this inconsistently.

This was previously flagged and marked as addressed in commit 3d2e35a, but "x" is still present.

Apply this diff:

         [Test]
-        [TestCase("x")]
         [TestCase("🠷")]
         [TestCase("βš™")]
         [TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
         [TestCase("☰")]
         public void GetgraphemesEmojiWithTextPresentation(string text)

143-160: Critical: "x" does not support emoji presentation.

The test case includes "x" (line 144) with U+FE0F (emoji variation selector) and expects width=2. This is invalid because "x" is a basic Latin letter that cannot be rendered as a double-wide emoji. Terminals will treat this as "x" (width 1), causing the assertion to fail.

Apply this diff:

         [Test]
-        [TestCase("x")]
         [TestCase("🠷")]
         [TestCase("βš™")]
         [TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
         [TestCase("☰")]
         public void GetgraphemesEmojiWithEmojiPresentation(string text)
🧹 Nitpick comments (2)
src/Consolonia.Core/Text/TextShaper.cs (1)

3-4: Remove unused using directives.

System.Diagnostics and System.Linq are not used in this file. The Debug.WriteLine calls that would have used Diagnostics are commented out.

Apply this diff:

 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
 using Avalonia;
 using Avalonia.Media;
src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)

14-19: Consider making Grapheme properties init-only.

The Grapheme class properties are currently mutable. Since graphemes are created once and not modified, consider using init accessors to make instances immutable after construction, preventing accidental modification.

Apply this diff:

     public class Grapheme
     {
-        public string Text { get; set; }
+        public string Text { get; init; }
 
-        public int Cluster { get; set; }
+        public int Cluster { get; init; }
     }
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 9927ed6 and 8890e5a.

πŸ“’ Files selected for processing (4)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs (2 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
  • src/Consolonia.Core/Text/TextShaper.cs (2 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (2)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs
  • src/Consolonia.Core/Text/TextShaper.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
src/Tests/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

Write tests using NUnit (project uses NUnit with custom Consolonia test base classes)

Files:

  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
🧬 Code graph analysis (3)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (5)
src/Consolonia.PlatformSupport/Helpers.cs (1)
  • Helpers (7-18)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Core/Infrastructure/ConsoleBase.cs (3)
  • WriteText (191-195)
  • SetCaretPosition (171-174)
  • PixelBufferCoordinate (145-148)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (1)
  • Grapheme (14-19)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
src/Consolonia.Core/Text/TextShaper.cs (2)
src/Consolonia.Core/Helpers/UtilityExtensions.cs (2)
  • IReadOnlyList (77-198)
  • Grapheme (14-19)
src/Consolonia.Core/Text/ConsoleTypeface.cs (3)
  • ConsoleTypeface (12-133)
  • GetGlyphIndex (50-66)
  • GetGlyphAdvance (86-92)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (4)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/SimpleSymbolTests.cs (16)
  • Test (13-19)
  • Test (21-27)
  • Test (29-35)
  • Test (37-45)
  • Test (47-54)
  • Test (56-64)
  • Test (66-73)
  • Test (75-83)
  • Test (85-92)
  • Test (94-106)
  • Test (108-114)
  • Test (116-124)
  • Test (126-134)
  • Test (136-144)
  • Test (146-154)
  • Test (156-163)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)
  • Symbol (172-183)
src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1)
  • Symbol (8-24)
πŸ”‡ Additional comments (8)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (3)

24-29: LGTM: Clean capability detection pattern.

The nullable bool fields with properties that default to false provide a clean pattern for detecting and exposing terminal capabilities. This allows the detection to run during PrepareConsole while providing safe defaults.


86-114: LGTM: Properly refactored rendering paths.

The dual rendering paths correctly handle terminals with and without emoji variation support:

  • The fast path (lines 86-91) writes the entire string when variations are supported
  • The fallback path (lines 92-111) iterates through graphemes and manually positions/clears

Both paths correctly update bufferPoint and set _headBufferPoint at the end. The space-clearing logic properly bounds the write to avoid off-screen writes.


142-147: LGTM: Correct emoji variation detection.

The detection correctly:

  1. Tests complex emoji support by writing a family emoji and checking if cursor advances by 2
  2. Tests variation selector support by writing wastebasket emoji with U+FE0F and checking if cursor advances by 2

This properly distinguishes terminals that honor variation selectors from those that ignore them.

src/Consolonia.Core/Helpers/UtilityExtensions.cs (4)

115-139: LGTM: Correct regional indicator pairing.

The regional indicator logic properly:

  • Tracks parity with regionalRuneCount
  • Only completes a pair when buffer is non-empty (line 119)
  • Starts a new RI sequence when buffer is empty or on odd count
  • Properly updates cluster indices for multi-codepoint sequences

This correctly handles flag emoji like πŸ‡ΊπŸ‡Έ as single graphemes.


140-154: LGTM: Proper variation selector attachment.

Variation selectors (U+FE0E text, U+FE0F emoji) are correctly appended to:

  1. The current emoji buffer if one is being built
  2. The last completed grapheme if no buffer is active

This ensures variation selectors modify the presentation of their preceding character rather than appearing as standalone entities.


155-178: LGTM: Correct emoji buffering and state management.

The emoji detection logic properly:

  • Continues multi-part emoji sequences after ZWJ/ORC when supported
  • Flushes the current emoji buffer before starting a new emoji
  • Resets regionalRuneCount when starting non-RI emoji (lines 170, 175)
  • Updates cluster indices for proper text layout

This ensures emoji sequences are correctly grouped into single graphemes.


179-190: LGTM: Correct text character handling.

The else branch properly:

  • Flushes any in-progress emoji buffer before adding text
  • Creates a standalone grapheme for the current rune
  • Updates cluster index to account for multi-byte UTF-16 sequences

The past review about resetting regionalRuneCount here was marked as addressed in commit 9cdc3fb.

src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (1)

224-301: LGTM: Comprehensive regional indicator test coverage.

The regional indicator tests provide excellent coverage:

  • Single flag with surrounding text (lines 224-243)
  • Multiple consecutive flags (lines 245-258)
  • Flags mixed with text (lines 260-285)
  • Parameterized tests for various country flags (lines 287-301)

All tests verify both Text content and Cluster indices, ensuring correct grapheme segmentation and layout positioning.

Comment on lines 27 to 32
var glyphTypeface = options.Typeface as ConsoleTypeface;
for (ushort i = 0; i < shapedBuffer.Length; i++)
{
var grapheme = graphemes[i];
var glyphIndex = glyphTypeface.GetGlyphIndex(grapheme.Text);
var glyphWidth = glyphTypeface.GetGlyphAdvance(glyphIndex);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add null check for ConsoleTypeface cast.

The cast options.Typeface as ConsoleTypeface at line 27 can return null if the typeface is not a ConsoleTypeface. This would cause a NullReferenceException when calling GetGlyphIndex or GetGlyphAdvance at lines 31-32.

Apply this diff:

-            var glyphTypeface = options.Typeface as ConsoleTypeface;
+            var glyphTypeface = options.Typeface as ConsoleTypeface 
+                ?? throw new InvalidOperationException("Typeface must be ConsoleTypeface");
             for (ushort i = 0; i < shapedBuffer.Length; i++)
πŸ€– Prompt for AI Agents
In src/Consolonia.Core/Text/TextShaper.cs around lines 27 to 32, the result of
casting options.Typeface to ConsoleTypeface can be null and will cause a
NullReferenceException when GetGlyphIndex/GetGlyphAdvance are called; add a
null-check immediately after the cast and either throw a clear
InvalidOperationException (including the actual runtime type of
options.Typeface) or handle the non-ConsoleTypeface case appropriately (e.g.,
convert/resolve to a ConsoleTypeface or skip shaping). Replace the current
direct usage with a checked variable (e.g., ensure options.Typeface is
ConsoleTypeface glyphTypeface or throw) before the loop so subsequent calls to
GetGlyphIndex and GetGlyphAdvance are safe.

Comment on lines 13 to 301
public void GetgraphemesEmptyStringReturnsEmptyList()
{
string text = string.Empty;
IReadOnlyList<string> glyphs = text.GetGlyphs(true);
Assert.IsEmpty(glyphs);
var graphemes = text.GetGraphemes(true);
Assert.IsEmpty(graphemes);
}

[Test]
public void GetGlyphsSingleCharacterReturnsSingleGlyph()
public void GetgraphemesSingleCharacterReturnsSingleGlyph()
{
string text = "a";
Assert.AreEqual(1, text.MeasureText());

IReadOnlyList<string> glyphs = text.GetGlyphs(true);
Assert.AreEqual(1, glyphs.Count);
Assert.AreEqual("a", glyphs[0]);
var graphemes = text.GetGraphemes(true);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual("a", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
}

[Test]
public void GetGlyphsMultipleCharsReturnsMultipleGlyph()
public void GetgraphemesMultipleCharsReturnsMultipleGlyph()
{
string text = "hello";
Assert.AreEqual(5, text.MeasureText());

IReadOnlyList<string> glyphs = text.GetGlyphs(true);
Assert.AreEqual(5, glyphs.Count);
Assert.AreEqual("h", glyphs[0]);
Assert.AreEqual("e", glyphs[1]);
Assert.AreEqual("l", glyphs[2]);
Assert.AreEqual("l", glyphs[3]);
Assert.AreEqual("o", glyphs[4]);
var graphemes = text.GetGraphemes(true);
Assert.AreEqual(5, graphemes.Count);
Assert.AreEqual("h", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
Assert.AreEqual("e", graphemes[1].Text);
Assert.AreEqual(1, graphemes[1].Cluster);
Assert.AreEqual("l", graphemes[2].Text);
Assert.AreEqual(2, graphemes[2].Cluster);
Assert.AreEqual("l", graphemes[3].Text);
Assert.AreEqual(3, graphemes[3].Cluster);
Assert.AreEqual("o", graphemes[4].Text);
Assert.AreEqual(4, graphemes[4].Cluster);
}

[Test]
public void GetGlyphsComplexCharsReturnsSingleGlyph()
public void GetgraphemesComplexCharsReturnsSingleGlyph()
{
string text = "π”‰π”žπ”«π” π”Ά";
Assert.AreEqual(5, text.MeasureText());

IReadOnlyList<string> glyphs = text.GetGlyphs(true);
Assert.AreEqual(5, glyphs.Count);
Assert.AreEqual("𝔉", glyphs[0]);
Assert.AreEqual("π”ž", glyphs[1]);
Assert.AreEqual("𝔫", glyphs[2]);
Assert.AreEqual("𝔠", glyphs[3]);
Assert.AreEqual("𝔢", glyphs[4]);
var graphemes = text.GetGraphemes(true);
Assert.AreEqual(5, graphemes.Count);
Assert.AreEqual("𝔉", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
Assert.AreEqual("π”ž", graphemes[1].Text);
Assert.AreEqual(2, graphemes[1].Cluster);
Assert.AreEqual("𝔫", graphemes[2].Text);
Assert.AreEqual(4, graphemes[2].Cluster);
Assert.AreEqual("𝔠", graphemes[3].Text);
Assert.AreEqual(6, graphemes[3].Cluster);
Assert.AreEqual("𝔢", graphemes[4].Text);
Assert.AreEqual(8, graphemes[4].Cluster);
}

[Test]
public void GetGlyphsSingleEmojiReturnsSingleGlyph()
public void GetgraphemesSingleEmojiReturnsSingleGlyph()
{
string text = "πŸ‘";
Assert.AreEqual(2, text.MeasureText());

IReadOnlyList<string> glyphs = text.GetGlyphs(true);
Assert.AreEqual(1, glyphs.Count);
Assert.AreEqual("πŸ‘", glyphs[0]);
var graphemes = text.GetGraphemes(true);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual("πŸ‘", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
}

[Test]
public void GetGlyphsWithComplexEmoji()
[TestCase("1\uFE0f\u20e3")]
[TestCase("πŸ‘πŸ»")]
[TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
public void GetgraphemesWithComplexEmoji(string text)
{
string text = "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦";
Assert.AreEqual(2, text.MeasureText());

IReadOnlyList<string> glyphs = text.GetGlyphs(true);
Assert.AreEqual(1, glyphs.Count);
Assert.AreEqual("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦", glyphs[0]);
var graphemes = text.GetGraphemes(true);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual(text, graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
}

[Test]
public void GetGlyphsWithMultipleGlyphs()
public void GetgraphemesWithMultiplegraphemes()
{
string text = "aπŸ‘";
Assert.AreEqual(3, text.MeasureText());

IReadOnlyList<string> glyphs = text.GetGlyphs(true);
Assert.AreEqual(2, glyphs.Count);
Assert.AreEqual("a", glyphs[0]);
Assert.AreEqual("πŸ‘", glyphs[1]);
var graphemes = text.GetGraphemes(true);
Assert.AreEqual(2, graphemes.Count);
Assert.AreEqual("a", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
Assert.AreEqual("πŸ‘", graphemes[1].Text);
Assert.AreEqual(1, graphemes[1].Cluster);
}

[Test]
public void GetGlyphsWithOutComplexEmojiSupport()
public void GetgraphemesWithOutComplexEmojiSupport()
{
string text = "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦";
Assert.AreEqual(2, text.MeasureText());

IReadOnlyList<string> glyphs = text.GetGlyphs(false);
Assert.AreEqual(4, glyphs.Count);
Assert.AreEqual("πŸ‘¨", glyphs[0]);
Assert.AreEqual("πŸ‘©", glyphs[1]);
Assert.AreEqual("πŸ‘§", glyphs[2]);
Assert.AreEqual("πŸ‘¦", glyphs[3]);
var graphemes = text.GetGraphemes(false);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual("πŸ‘¨", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
}

[Test]
[TestCase("x")]
[TestCase("🠷")]
[TestCase("βš™")]
[TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
[TestCase("☰")]
public void GetgraphemesEmojiWithTextPresentation(string text)
{
// Emoji followed by FE0E (text presentation selector)
// The emoji should be rendered as text (single-wide)
text = $"{text}\uFE0E";
Assert.AreEqual(1, text.MeasureText());

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual(text, graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
}

[Test]
[TestCase("x")]
[TestCase("🠷")]
[TestCase("βš™")]
[TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
[TestCase("☰")]
public void GetgraphemesEmojiWithEmojiPresentation(string text)
{
// Emoji followed by FE0F (emoji presentation selector)
// The emoji should be rendered as emoji (double-wide)
text = $"{text}\uFE0F";
Assert.AreEqual(2, text.MeasureText());

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual(text, graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
}

[Test]
[TestCase("βš™")]
[TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
[TestCase("☰")]
public void GetgraphemesAutoEmojiPresentation(string text)
{
// πŸ—™ (U+1F5D9) followed by FE0F (emoji presentation selector)
Assert.AreEqual(2, text.MeasureText());

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual(text, graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);

var symbol = new Symbol(graphemes[0].Text);
Assert.AreEqual($"{text}\ufe0f", symbol.Complex);
}

[Test]
public void GetgraphemesCancelSignWithTextPresentation()
{
// πŸ—™ (U+1F5D9) followed by FE0E (text presentation selector)
string text = "πŸ—™\uFE0E";
Assert.AreEqual(1, text.MeasureText());

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual("πŸ—™\uFE0E", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
}

[Test]
public void GetgraphemesMultipleEmojisWithVariationSelectors()
{
// Mix of emojis with variation selectors
string text = "☺\uFE0E☺\uFE0F";
Assert.AreEqual(3, text.MeasureText()); // 1 (text) + 2 (emoji)

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(2, graphemes.Count);
Assert.AreEqual("☺\uFE0E", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
Assert.AreEqual("☺\uFE0F", graphemes[1].Text);
Assert.AreEqual(2, graphemes[1].Cluster);
}

[Test]
public void GetgraphemesMultipleEmojisWithoutVariationSelectors()
{
// Mix of emojis with variation selectors
string text = "πŸ³οΈβ€πŸŒˆπŸ³οΈβ€πŸŒˆ";
Assert.AreEqual(4, text.MeasureText());

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(2, graphemes.Count);
Assert.AreEqual("πŸ³οΈβ€πŸŒˆ", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
Assert.AreEqual("πŸ³οΈβ€πŸŒˆ", graphemes[1].Text);
Assert.AreEqual(6, graphemes[1].Cluster);
}

// Add regional indicator tests below
[Test]
public void GetgraphemesRegionalIndicatorSingleFlag()
{
// US flag: πŸ‡ΊπŸ‡Έ (U+1F1FA U+1F1F8)
string text = "πŸ‡ΊπŸ‡Έflag";
Assert.AreEqual(6, text.MeasureText());

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(5, graphemes.Count);
Assert.AreEqual("πŸ‡ΊπŸ‡Έ", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
Assert.AreEqual("f", graphemes[1].Text);
Assert.AreEqual(4, graphemes[1].Cluster);
Assert.AreEqual("l", graphemes[2].Text);
Assert.AreEqual(5, graphemes[2].Cluster);
Assert.AreEqual("a", graphemes[3].Text);
Assert.AreEqual(6, graphemes[3].Cluster);
Assert.AreEqual("g", graphemes[4].Text);
Assert.AreEqual(7, graphemes[4].Cluster);
}

[Test]
public void GetgraphemesRegionalIndicatorMultipleFlags()
{
// US flag + UK flag: πŸ‡ΊπŸ‡ΈπŸ‡¬πŸ‡§
string text = "πŸ‡ΊπŸ‡ΈπŸ‡¬πŸ‡§";
Assert.AreEqual(4, text.MeasureText());

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(2, graphemes.Count);
Assert.AreEqual("πŸ‡ΊπŸ‡Έ", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
Assert.AreEqual("πŸ‡¬πŸ‡§", graphemes[1].Text);
Assert.AreEqual(4, graphemes[1].Cluster);
}

[Test]
public void GetgraphemesRegionalIndicatorWithText()
{
// Text with flag: "Hello πŸ‡ΊπŸ‡Έ"
string text = "Hello πŸ‡ΊπŸ‡Έ!";
Assert.AreEqual(9, text.MeasureText()); // H-e-l-l-o-space + flag(2)

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(8, graphemes.Count);
Assert.AreEqual("H", graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
Assert.AreEqual("e", graphemes[1].Text);
Assert.AreEqual(1, graphemes[1].Cluster);
Assert.AreEqual("l", graphemes[2].Text);
Assert.AreEqual(2, graphemes[2].Cluster);
Assert.AreEqual("l", graphemes[3].Text);
Assert.AreEqual(3, graphemes[3].Cluster);
Assert.AreEqual("o", graphemes[4].Text);
Assert.AreEqual(4, graphemes[4].Cluster);
Assert.AreEqual(" ", graphemes[5].Text);
Assert.AreEqual(5, graphemes[5].Cluster);
Assert.AreEqual("πŸ‡ΊπŸ‡Έ", graphemes[6].Text);
Assert.AreEqual(6, graphemes[6].Cluster);
Assert.AreEqual("!", graphemes[7].Text);
Assert.AreEqual(10, graphemes[7].Cluster);
}

[Test]
[TestCase("πŸ‡¦πŸ‡Ί")] // Australia
[TestCase("πŸ‡¨πŸ‡¦")] // Canada
[TestCase("πŸ‡©πŸ‡ͺ")] // Germany
[TestCase("πŸ‡«πŸ‡·")] // France
[TestCase("πŸ‡―πŸ‡΅")] // Japan
public void GetgraphemesRegionalIndicatorVariousFlags(string text)
{
Assert.AreEqual(2, text.MeasureText());

var graphemes = text.GetGraphemes(true);
Assert.AreEqual(1, graphemes.Count);
Assert.AreEqual(text, graphemes[0].Text);
Assert.AreEqual(0, graphemes[0].Cluster);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Fix test method naming: Use PascalCase "GetGraphemes" instead of "Getgraphemes".

All test methods use Getgraphemes (lowercase 'g' in the middle) instead of GetGraphemes (proper PascalCase). This violates C# naming conventions and is inconsistent with the actual method name being tested.

Apply this pattern throughout the file:

-        public void GetgraphemesEmptyStringReturnsEmptyList()
+        public void GetGraphemesEmptyStringReturnsEmptyList()

-        public void GetgraphemesSingleCharacterReturnsSingleGlyph()
+        public void GetGraphemesSingleCharacterReturnsSingleGlyph()

And similarly for all other test method names in this file.

Based on coding guidelines: Fix IDE1006 naming rule violations manually.

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (lines
13-301), several test method identifiers use "Getgraphemes" (lowercase 'g')
violating PascalCase naming (IDE1006); rename each test method identifier to use
"GetGraphemes" (e.g., GetgraphemesEmptyStringReturnsEmptyList ->
GetGraphemesEmptyStringReturnsEmptyList) preserving attributes, method bodies
and signatures, and update any local references/usages inside the file to the
new names to satisfy naming conventions.

tomlm added 3 commits October 25, 2025 09:47
Structural Improvements:
1. Extracted Helper Methods: The massive if-else chain has been replaced with:
β€’ ClassifyRune() - Categorizes each rune into a type
β€’ HandleZeroWidthJoiner() - Handles ZWJ/ORC characters
β€’ HandleRegionalIndicator() - Handles flag emoji logic
β€’ HandleEmoji() - Handles emoji processing
β€’ AppendToCurrentOrLastGlyph() - Reusable append logic
β€’ FlushBufferIfNeeded() - Centralizes buffer flushing
β€’ Helper predicate methods: IsModifier(), IsRegionalIndicator(), IsVariationSelector()
2. Introduced RuneType Enum: Makes the code self-documenting by explicitly naming the different character categories
3. Switch Statement: Replaced nested if-else with a cleaner switch statement for handling each rune type
4. Simplified Variable Names:
β€’ emoji β†’ buffer (more generic and accurate)
β€’ lastRune β†’ previousRune (clearer naming)
β€’ regionalRuneCount β†’ regionalIndicatorCount (more precise)
5. Improved Readability: Each case in the switch is concise, and the complex logic is encapsulated in appropriately named helper methods
Technical Benefits:
β€’ Maintainability: Much easier to understand and modify individual aspects of grapheme parsing
β€’ Testability: Helper methods can be unit tested independently
β€’ Debugging: Clearer call stack when issues occur
β€’ Documentation: The enum and method names serve as inline documentation
@github-actions
Copy link
Contributor

PR diff hash: 0e6fcfe2ce0be544d87186fd1e42274a69535f01749ea992acf414e18237a377

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2)

124-131: Remove β€œx” from FE0E/FE0F tests; it doesn’t support variation selectors

"x" has no emoji/text presentation; expecting width 2 for "x\uFE0F" is invalid and will fail on compliant terminals.

Apply:

@@
-        [TestCase("x")]
         [TestCase("🠷")]
         [TestCase("βš™")]
         [TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
         [TestCase("☰")]
@@
-        [TestCase("x")]
         [TestCase("🠷")]
         [TestCase("βš™")]
         [TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
         [TestCase("☰")]

Also applies to: 143-160


208-221: Fix misleading test: sample contains VS16; either rename or change data

Current sample "πŸ³οΈβ€πŸŒˆπŸ³οΈβ€πŸŒˆ" includes U+FE0F. Prefer using flags (no VS) to match the name.

Apply:

-        public void GetgraphemesMultipleEmojisWithoutVariationSelectors()
+        public void GetGraphemesMultipleFlagsWithoutVariationSelectors()
         {
-            // Mix of emojis with variation selectors
-            string text = "πŸ³οΈβ€πŸŒˆπŸ³οΈβ€πŸŒˆ";
+            // Two regional-indicator flags (no variation selectors)
+            string text = "πŸ‡©πŸ‡ͺπŸ‡©πŸ‡ͺ";
             Assert.AreEqual(4, text.MeasureText());
 
             var graphemes = Grapheme.Parse(text, true);
             Assert.AreEqual(2, graphemes.Count);
-            Assert.AreEqual("πŸ³οΈβ€πŸŒˆ", graphemes[0].Text);
+            Assert.AreEqual("πŸ‡©πŸ‡ͺ", graphemes[0].Text);
             Assert.AreEqual(0, graphemes[0].Cluster);
-            Assert.AreEqual("πŸ³οΈβ€πŸŒˆ", graphemes[1].Text);
-            Assert.AreEqual(6, graphemes[1].Cluster);
+            Assert.AreEqual("πŸ‡©πŸ‡ͺ", graphemes[1].Text);
+            Assert.AreEqual(2, graphemes[1].Cluster);
         }
src/Consolonia.Core/Text/TextShaper.cs (1)

27-36: Null-cast risk and ushort loop overflow; also cast advance to double

  • options.Typeface as ConsoleTypeface can be null β†’ NRE on GetGlyphIndex/GetGlyphAdvance.
  • for (ushort i ...) can wrap at 65535 β†’ hang for long inputs.
  • GlyphInfo third arg expects double; pass explicit double for clarity.

Apply:

-            var glyphTypeface = options.Typeface as ConsoleTypeface;
-            for (ushort i = 0; i < shapedBuffer.Length; i++)
+            var glyphTypeface = options.Typeface as ConsoleTypeface
+                ?? throw new InvalidOperationException($"Typeface must be {nameof(ConsoleTypeface)}, got {options.Typeface?.GetType().Name ?? "null"}");
+            for (int i = 0; i < shapedBuffer.Length; i++)
             {
                 var grapheme = graphemes[i];
                 var glyphIndex = glyphTypeface.GetGlyphIndex(grapheme.Text);
-                var glyphWidth = glyphTypeface.GetGlyphAdvance(glyphIndex);
+                var glyphAdvance = (double)glyphTypeface.GetGlyphAdvance(glyphIndex);
                 // NOTE: We are using the placeholder glyph since we are pushing
                 // raw text to the console and not using a font system to render the text
-                shapedBuffer[i] = new GlyphInfo(glyphIndex, grapheme.Cluster, glyphWidth);
+                shapedBuffer[i] = new GlyphInfo(glyphIndex, grapheme.Cluster, glyphAdvance);
             }
🧹 Nitpick comments (1)
src/Consolonia.Core/Helpers/Grapheme.cs (1)

121-125: Make HandleZeroWidthJoiner return void

The bool return value isn’t used. Simplify to void and remove the return for clarity.

-        private static bool HandleZeroWidthJoiner(Rune rune, StringBuilder buffer, List<Grapheme> glyphs)
+        private static void HandleZeroWidthJoiner(Rune rune, StringBuilder buffer, List<Grapheme> glyphs)
         {
             AppendToCurrentOrLastGlyph(rune, buffer, glyphs);
-            return true;
         }
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 8890e5a and a8f79ea.

πŸ“’ Files selected for processing (5)
  • src/Consolonia.Core/Helpers/Grapheme.cs (1 hunks)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs (1 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
  • src/Consolonia.Core/Text/TextShaper.cs (2 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Consolonia.Core/Helpers/UtilityExtensions.cs
🧰 Additional context used
πŸ““ Path-based instructions (2)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Helpers/Grapheme.cs
  • src/Consolonia.Core/Text/TextShaper.cs
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
src/Tests/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

Write tests using NUnit (project uses NUnit with custom Consolonia test base classes)

Files:

  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
🧬 Code graph analysis (3)
src/Consolonia.Core/Text/TextShaper.cs (2)
src/Consolonia.Core/Helpers/Grapheme.cs (2)
  • IReadOnlyList (20-78)
  • Grapheme (7-196)
src/Consolonia.Core/Text/ConsoleTypeface.cs (3)
  • ConsoleTypeface (12-133)
  • GetGlyphIndex (50-66)
  • GetGlyphAdvance (86-92)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (5)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (3)
  • WriteText (83-83)
  • SetCaretPosition (27-27)
  • PixelBufferCoordinate (33-33)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Helpers/Grapheme.cs (1)
  • Grapheme (7-196)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2)
src/Consolonia.Core/Helpers/Grapheme.cs (1)
  • Grapheme (7-196)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: inspection
πŸ”‡ Additional comments (2)
src/Consolonia.Core/Text/TextShaper.cs (1)

24-26: Confirm ShapedBuffer ctor parameters (bidi level hardcoded)

You pass 1 and 0 with a TODO about RTL. Please wire the actual bidi/flow from options (or document why constants are correct) to avoid RTL regressions.

src/Consolonia.Core/Helpers/Grapheme.cs (1)

35-42: Early return truncates remainder of input when ZWJ is seen without complex emoji

On encountering ZWJ with supportsComplexEmoji == false you FlushBufferIfNeeded(...) and return glyphs, dropping the rest of the string. Is this intentional? If not, consider flushing the current grapheme and continuing by treating subsequent runes as separate graphemes.

Comment on lines +99 to +104
if (glyphWidth > 1)
{
WriteText(Esc.SetCursorPosition(bufferPoint.X + 1, bufferPoint.Y));
WriteText(new string(' ', Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1)));
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clamp clear width to non-negative to avoid ArgumentOutOfRangeException

When near/right of edge, the first term can be negative. Clamp before creating the string.

-                        WriteText(new string(' ', Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1)));
+                        var clearCount = Math.Max(0, Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1));
+                        if (clearCount > 0)
+                            WriteText(new string(' ', clearCount));

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs around lines 99 to
104, the call new string(' ', Math.Min(Size.Width - bufferPoint.X - 1,
glyphWidth - 1)) can receive a negative count when near the right edge; clamp
the computed clear width to be non-negative (e.g. compute int clear =
Math.Max(0, Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1))) and use
that value when creating the string so you never call new string with a negative
length and avoid ArgumentOutOfRangeException.

Comment on lines +13 to 14
public void GetgraphemesEmptyStringReturnsEmptyList()
{
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Rename test methods to PascalCase (IDE1006)

Use GetGraphemes... for consistency and to satisfy naming rules.

Examples:

-        public void GetgraphemesEmptyStringReturnsEmptyList()
+        public void GetGraphemesEmptyStringReturnsEmptyList()

-        public void GetgraphemesSingleCharacterReturnsSingleGlyph()
+        public void GetGraphemesSingleCharacterReturnsSingleGlyph()

-        public void GetgraphemesMultipleCharsReturnsMultipleGlyph()
+        public void GetGraphemesMultipleCharsReturnsMultipleGraphemes()

-        public void GetgraphemesComplexCharsReturnsSingleGlyph()
+        public void GetGraphemesComplexCharsReturnMultipleGraphemes()

-        public void GetgraphemesSingleEmojiReturnsSingleGlyph()
+        public void GetGraphemesSingleEmojiReturnsSingleGrapheme()

-        public void GetgraphemesWithComplexEmoji(string text)
+        public void GetGraphemesWithComplexEmoji(string text)

-        public void GetgraphemesWithMultiplegraphemes()
+        public void GetGraphemesWithMultipleGraphemes()

-        public void GetgraphemesWithOutComplexEmojiSupport()
+        public void GetGraphemesWithoutComplexEmojiSupport()

-        public void GetgraphemesEmojiWithTextPresentation(string text)
+        public void GetGraphemesEmojiWithTextPresentation(string text)

-        public void GetgraphemesEmojiWithEmojiPresentation(string text)
+        public void GetGraphemesEmojiWithEmojiPresentation(string text)

-        public void GetgraphemesAutoEmojiPresentation(string text)
+        public void GetGraphemesAutoEmojiPresentation(string text)

-        public void GetgraphemesCancelSignWithTextPresentation()
+        public void GetGraphemesCancelSignWithTextPresentation()

-        public void GetgraphemesMultipleEmojisWithVariationSelectors()
+        public void GetGraphemesMultipleEmojisWithVariationSelectors()

-        public void GetgraphemesMultipleEmojisWithoutVariationSelectors()
+        public void GetGraphemesMultipleZWJEmojis()

-        public void GetgraphemesRegionalIndicatorSingleFlag()
+        public void GetGraphemesRegionalIndicatorSingleFlag()

-        public void GetgraphemesRegionalIndicatorMultipleFlags()
+        public void GetGraphemesRegionalIndicatorMultipleFlags()

-        public void GetgraphemesRegionalIndicatorWithText()
+        public void GetGraphemesRegionalIndicatorWithText()

-        public void GetgraphemesRegionalIndicatorVariousFlags(string text)
+        public void GetGraphemesRegionalIndicatorVariousFlags(string text)

Also applies to: 21-22, 33-34, 53-54, 73-74, 98-100, 112-114, 124-131, 143-160, 162-167, 180-182, 193-195, 208-210, 223-301

πŸ€– Prompt for AI Agents
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs around lines
13-301, several test method names use camelCase (e.g.,
GetgraphemesEmptyStringReturnsEmptyList) and should be renamed to PascalCase
(GetGraphemes...) to satisfy IDE1006; update each method declaration listed in
the comment (lines 13-14, 21-22, 33-34, 53-54, 73-74, 98-100, 112-114, 124-131,
143-160, 162-167, 180-182, 193-195, 208-210, 223-301) to PascalCase and rename
any references or usages (test runner attributes, helper calls) accordingly so
signatures and references remain consistent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2)

13-14: Rename test methods to PascalCase (IDE1006).

Use GetGraphemes… consistently.

Example pattern:

-        public void GetgraphemesEmptyStringReturnsEmptyList()
+        public void GetGraphemesEmptyStringReturnsEmptyList()

Apply similarly across the file (methods shown in the line ranges).

Also applies to: 21-22, 33-34, 53-54, 73-74, 98-100, 112-114, 124-131, 143-160, 180-182, 193-195, 208-210


124-131: Remove invalid β€œx” FE0E/FE0F testcases (does not support emoji variation).

Including "x" makes expectations incorrect and platform-dependent.

-        [TestCase("x")]
         [TestCase("🠷")]
         [TestCase("βš™")]
         [TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
         [TestCase("☰")]
@@
-        [TestCase("x")]
         [TestCase("🠷")]
         [TestCase("βš™")]
         [TestCase("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦")]
         [TestCase("☰")]

If desired, replace with a VS-capable base (e.g., "☺").

Also applies to: 143-160

src/Consolonia.Core/Text/TextShaper.cs (2)

28-36: Use int loop index to avoid wrap/hang for long inputs.

ushort wraps at 65535; can hang for Length >= 65536.

-            for (ushort i = 0; i < shapedBuffer.Length; i++)
+            for (int i = 0; i < shapedBuffer.Length; i++)
             {
                 var grapheme = graphemes[i];
                 var glyphIndex = glyphTypeface.GetGlyphIndex(grapheme.Glyph);
                 var glyphWidth = glyphTypeface.GetGlyphAdvance(glyphIndex);
                 shapedBuffer[i] = new GlyphInfo(glyphIndex, grapheme.Cluster, glyphWidth);
             }

27-27: Null-check the ConsoleTypeface cast.

Avoid NRE if a different IGlyphTypeface is provided.

-            var glyphTypeface = options.Typeface as ConsoleTypeface;
+            var glyphTypeface = options.Typeface as ConsoleTypeface
+                ?? throw new InvalidOperationException(
+                    $"Typeface must be ConsoleTypeface, got {options.Typeface?.GetType().Name ?? "null"}");
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (1)

99-106: Clamp space-clear count; avoid negative lengths near right edge.

Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1) can be negative when bufferPoint.X >= Size.Width. This will throw in new string.

-                    if (glyphWidth > 1)
-                    {
-                        WriteText(Esc.SetCursorPosition(bufferPoint.X + 1, bufferPoint.Y));
-                        WriteText(new string(' ', Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1)));
-                    }
+                    if (glyphWidth > 1 && bufferPoint.X + 1 < Size.Width)
+                    {
+                        WriteText(Esc.SetCursorPosition(bufferPoint.X + 1, bufferPoint.Y));
+                        var clear = Math.Max(0, Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1));
+                        if (clear > 0)
+                            WriteText(new string(' ', clear));
+                    }
🧹 Nitpick comments (3)
src/Consolonia.Core/Helpers/Grapheme.cs (1)

128-133: Make HandleZeroWidthJoiner return void (return value unused).

Return value isn’t consumed; simplify signature and body.

-        private static bool HandleZeroWidthJoiner(Rune rune, StringBuilder buffer, List<Grapheme> glyphs)
+        private static void HandleZeroWidthJoiner(Rune rune, StringBuilder buffer, List<Grapheme> glyphs)
         {
             AppendToCurrentOrLastGlyph(rune, buffer, glyphs);
-            return true;
         }
src/Consolonia.Core/Text/TextShaper.cs (1)

3-7: Remove unused usings (keeps build clean).

These are unused after commenting out debug lines.

-using System.Diagnostics;
-using System.Linq;
 using Avalonia;
 using Avalonia.Media;
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (1)

112-114: Fix method name typo (β€œWithOut” β†’ β€œWithout”).

Keeps names consistent with the rest of the suite.

-        public void GetgraphemesWithOutComplexEmojiSupport()
+        public void GetGraphemesWithoutComplexEmojiSupport()
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between a8f79ea and e57159f.

πŸ“’ Files selected for processing (4)
  • src/Consolonia.Core/Helpers/Grapheme.cs (1 hunks)
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (4 hunks)
  • src/Consolonia.Core/Text/TextShaper.cs (2 hunks)
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (2 hunks)
🧰 Additional context used
πŸ““ Path-based instructions (2)
src/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.cs: C# code must be formatted per src/.editorconfig; run dotnet format so CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)

Files:

  • src/Consolonia.Core/Helpers/Grapheme.cs
  • src/Consolonia.Core/Text/TextShaper.cs
  • src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs
  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
src/Tests/**/*.cs

πŸ“„ CodeRabbit inference engine (.github/copilot-instructions.md)

Write tests using NUnit (project uses NUnit with custom Consolonia test base classes)

Files:

  • src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs
🧬 Code graph analysis (3)
src/Consolonia.Core/Text/TextShaper.cs (2)
src/Consolonia.Core/Helpers/Grapheme.cs (2)
  • IReadOnlyList (27-85)
  • Grapheme (10-203)
src/Consolonia.Core/Text/ConsoleTypeface.cs (3)
  • ConsoleTypeface (12-133)
  • GetGlyphIndex (50-66)
  • GetGlyphAdvance (86-92)
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cs (5)
src/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cs (3)
  • WriteText (73-76)
  • SetCaretPosition (35-46)
  • PixelBufferCoordinate (48-52)
src/Consolonia.Core/Infrastructure/IConsoleOutput.cs (3)
  • WriteText (83-83)
  • SetCaretPosition (27-27)
  • PixelBufferCoordinate (33-33)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Helpers/Grapheme.cs (1)
  • Grapheme (10-203)
src/Consolonia.Core/Text/Esc.cs (2)
  • Esc (9-189)
  • SetCursorPosition (92-95)
src/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cs (4)
src/Consolonia.Core/Helpers/Grapheme.cs (1)
  • Grapheme (10-203)
src/Consolonia.Controls/ControlUtils.cs (1)
  • MeasureText (33-108)
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs (1)
  • Symbol (172-183)
src/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cs (1)
  • Symbol (8-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: inspection
πŸ”‡ Additional comments (1)
src/Consolonia.Core/Helpers/Grapheme.cs (1)

42-49: Early return on ZWJ when complex emoji unsupported may truncate output.

This stops parsing at first ZWJ; printing will skip the rest of the string.

Would you prefer to:

  • emit the pre-ZWJ grapheme and then treat subsequent runes as independent (degraded) graphemes, or
  • keep truncation? If truncation is intentional, add a brief comment explaining the UX tradeoff.

Comment on lines +21 to +26
/// Process text into collection of grapheme where a graphemeis either text or a combination of chars which make up an
/// emoji
/// </summary>
/// <param name="text">text to get glyphs from</param>
/// <param name="supportsComplexEmoji">If true, emojis like πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ will be treated as a single glyph></param>
/// <returns></returns>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Fix doc typos and clarify summary/param text.

Minor XML doc issues and unclear phrasing.

-        ///     Process text into collection of grapheme where a graphemeis either text or a combination of chars which make up an
-        ///     emoji
+        ///     Process text into a collection of graphemes, where a grapheme is either a single character
+        ///     or a sequence of code points that compose an emoji.
@@
-        /// <param name="supportsComplexEmoji">If true, emojis like πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ will be treated as a single glyph></param>
+        /// <param name="supportsComplexEmoji">If true, sequences like πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ are treated as a single grapheme.</param>
πŸ€– Prompt for AI Agents
In src/Consolonia.Core/Helpers/Grapheme.cs around lines 21 to 26, the XML
documentation has typos and unclear phrasing; correct "graphemeis" to
"graphemes", tighten the summary to "Process text into a collection of graphemes
where a grapheme is either a single character or a sequence of characters that
form an emoji", fix param descriptions to be clear and grammatical (e.g.,
"<param name=\"text\">Text to extract graphemes from.</param>" and "<param
name=\"supportsComplexEmoji\">If true, treat sequences like πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ as a
single grapheme.</param>"), and add a brief <returns> description such as "A
collection of grapheme strings." Ensure XML tags are well-formed.

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.

2 participants