-
-
Couldn't load subscription status.
- Fork 21
Add support for Text and Emoji variation annotations #481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: pre-main
Are you sure you want to change the base?
Conversation
π WalkthroughWalkthroughAdds 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:
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 passed)
β¨ Finishing touches
π§ͺ Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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="β︎" />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 forpixel.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 subtractinglastWidth.
ushortsubtraction 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/lastWidthtointand 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).Widthto pin width expectations.
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Tests/Consolonia.Gallery.Tests/RadioButtonTests.cssrc/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelForegroundTests.cssrc/Consolonia.Core/Drawing/PixelBufferImplementation/SymbolConverter.cssrc/Consolonia.Controls/ControlUtils.cssrc/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelBufferTests.cssrc/Tests/Consolonia.Core.Tests/WithLifetimeFixture/SimpleSymbolTests.cssrc/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cssrc/Tests/Consolonia.Gallery.Tests/SliderTests.cssrc/Tests/Consolonia.Gallery.Tests/CheckBoxTests.cssrc/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cssrc/Consolonia.Core/Helpers/UtilityExtensions.cssrc/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.cssrc/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelForegroundTests.cssrc/Tests/Consolonia.Core.Tests/WithLifetimeFixture/PixelBufferTests.cssrc/Tests/Consolonia.Core.Tests/WithLifetimeFixture/SimpleSymbolTests.cssrc/Tests/Consolonia.Gallery.Tests/SliderTests.cssrc/Tests/Consolonia.Gallery.Tests/CheckBoxTests.cssrc/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
lastWidthin 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.
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
Outdated
Show resolved
Hide resolved
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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,
GlyphComplexCacheshould specifyStringComparer.Ordinalfor 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
ConcurrentDictionarywithGetOrAddas 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 outerglyph.The lambda parameter
glyphshadows 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,
Complexis alwaysnullor has length β₯ 2. The conditionComplex.Length > 1is 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Helpers/UtilityExtensions.cssrc/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.
src/Consolonia.Core/Drawing/PixelBufferImplementation/Symbol.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: <[email protected]>
There was a problem hiding this 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: Usechar.MinValueconsistently.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
π 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; rundotnet formatso 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() { ... }
There was a problem hiding this 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBuffer.cssrc/Consolonia.Core/Helpers/UtilityExtensions.cssrc/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.
There was a problem hiding this 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
DummyConsoleis used in testing scenarios where code may check theSupportsEmojiVariationcapability. ThrowingNotImplementedExceptioncould cause test failures. Consider returningfalse(ortrueif 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Dummy/DummyConsole.cssrc/Consolonia.NUnit/UnitTestConsole.cssrc/Consolonia.Core/Infrastructure/ConsoleBase.cssrc/Consolonia.Core/Drawing/RenderTarget.cssrc/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cssrc/Consolonia.Controls/IConsoleCapabilities.cssrc/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
SupportsComplexEmojion 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
SupportsComplexEmojiproperty 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
falseensures 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 usingConsole.WriteLine.
There was a problem hiding this 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 thenewconstructor 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
newconstructor 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
π 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; rundotnet formatso 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
_supportEmojifield into_supportsComplexEmojiand_supportsEmojiVariationappropriately separates two distinct terminal capabilities, making the code more maintainable.
139-144: LGTM! Well-designed terminal capability detection.The two-stage detection correctly identifies:
- Complex emoji support (composite surrogate pairs with ZWJ)
- Variation selector support (U+FE0F rendering)
Both tests use cursor position movement as a reliable indicator of terminal rendering capabilities. The sequential dependency on
left2is appropriate.
Co-authored-by: <[email protected]>
There was a problem hiding this 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: UseConsole.Write, notConsole.WriteLine, for cursor-delta detection.
WriteLineadds a newline and corrupts the width measurement. Switch toWrite.- (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, ignoringbufferPoint. 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.
UsebufferPoint.Ywhen advancing, not_headBufferPoint.Y. Also remove unusedsb.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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cssrc/Consolonia.Core/Infrastructure/DefaultNetConsoleOutput.cssrc/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 addusing 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 useWrite).
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 forMeasureText/GetGlyphsextensions.
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 usesWriteText(no newline), so deltas should be accurate; variation probe looks correct.
Test across terminals (Windows Terminal, macOS Terminal, Linux VTE) to confirm consistent deltas.
β¦e out the glyph then adavance the cursor. Slower but more compatible
β¦olonia into tomlm/AmibigiousChars
There was a problem hiding this 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Helpers/UtilityExtensions.cssrc/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cssrc/Tests/Consolonia.Core.Tests/WithLifetimeFixture/GlyphTests.cssrc/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)
Co-authored-by: <[email protected]>
There was a problem hiding this 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
π 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; rundotnet formatso 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 withSupportsComplexEmoji, resolving the previous concern about throwingNotImplementedException. This is appropriate for a dummy console implementation.
There was a problem hiding this 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:_headBufferPointupdated from stale value instead of actual write position.Lines 111-115 update
_headBufferPointbased on the old_headBufferPointvalue rather than thebufferPointwhere text was actually written. This breaks cursor tracking because:
- In the
SupportsEmojiVariationbranch, text is written atbufferPoint(not at_headBufferPoint)- In the else branch,
bufferPointis incrementally advanced to track the actual write position- But the final
_headBufferPointupdate ignoresbufferPointentirelyApply 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:bufferPointis never advanced in theSupportsEmojiVariationbranch.When
SupportsEmojiVariationis true, the code writes text atbufferPointbut never updatesbufferPointto reflect the new cursor position. This causes the subsequent_headBufferPointupdate (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: UseglyphWidthinstead oftextWidthfor clearing.Line 99 uses
textWidth(the width of the entire string) instead ofglyphWidth(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: useglyphs[^1]consistently for clarity.Replace
glyphs[glyphs.Count - 1]withglyphs[^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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Controls/ControlUtils.cssrc/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cssrc/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
GetGlyphsextension 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.
There was a problem hiding this 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.Counttoushortwill 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 ican 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
glyphWidthspaces; should clearglyphWidth - 1at 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Text/TextShaper.cssrc/Consolonia.Core/Helpers/UtilityExtensions.cssrc/Consolonia.Controls/ControlUtils.cssrc/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 theSupportsEmojiVariationbranch,SetCaretPosition()is called to sync state. The complex emoji branch manually tracksbufferPointduring glyph iteration, with_headBufferPointupdated 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.
β¦olonia into tomlm/AmibigiousChars
There was a problem hiding this 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
bufferPointis 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
After fixing the
bufferPointadvancement in theSupportsEmojiVariation=truebranch, 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=truepath writes the string but never advancesbufferPoint. This causes_headBufferPointtracking to break because the final update logic (lines 113-117) relies on either an advancedbufferPointor the previous_headBufferPointstate, leading to incorrect cursor positioning after this method returns.Apply this diff to advance
bufferPointafter 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
π 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; rundotnet formatso 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.
There was a problem hiding this 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
π 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; rundotnet formatso 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
There was a problem hiding this 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 forGlyphInfoconstructor.According to previous analysis,
GlyphInfoexpects(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance), but the call passesushortfor 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 inGetGlyphByIndex.The method now uses the correct lock (
GlyphCacheSync), but still uses direct dictionary indexing that will throwKeyNotFoundExceptionwithout context. Previous review suggested usingTryGetValuewith a clearer error message or adding a non-throwingTryGetGlyphByIndexAPI.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:ushortloop index will wrap and hang for long inputs.The loop index
ideclared asushortwill wrap at 65535 and spin indefinitely whenshapedBuffer.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.Counttoushortwithout 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Text/TextShaper.cssrc/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
GlyphCacheSyncobject 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) andSupportsEmojiVariation(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 = bufferPointwhich ensures consistent head tracking across both paths.
7-7: LGTM: Required import for glyph enumeration.The import of
Consolonia.Core.Helpersis needed for theGetGlyphsextension method used in the per-glyph rendering path (line 96).
There was a problem hiding this 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
π 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; rundotnet formatso 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 atbufferPoint.X + 1.
142-147: Emoji variation detection implemented correctly.The detection logic properly tests terminal support by:
- Writing a complex emoji and checking cursor advancement for
_supportsComplexEmoji(line 142).- 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.
Co-authored-by: <[email protected]>
There was a problem hiding this 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
SupportsEmojiVariationbranch, 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 emojiAlso applies to: 94-95
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π 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; rundotnet formatso 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
bufferPointand update_headBufferPointat the end. The space-clearing logic for wide glyphs (lines 99-103) is mathematically sound: it positions atbufferPoint.X + 1and writesMath.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.
β¦deling a typeface for consoles.
β¦olonia into tomlm/AmibigiousChars
There was a problem hiding this 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 GetGlyphAdvancesYou 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 cachesThese 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/stretchGood 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 simulationsIf Avalonia passes Bold/Italic simulations, consider mapping to Weight/Style or documenting that simulations are no-op.
57-57: Stream overload paritySame 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Text/FontManagerImpl.cssrc/Consolonia.Core/Drawing/DrawingContextImpl.cssrc/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.GlyphAdvanceto account for wide characters.- Dirty region calculation properly spans from start to final glyph position.
- The height calculation
position.Y - startPosition.Y + 1correctly handles single-line and multi-line scenarios.src/Consolonia.Core/Text/ConsoleTypeface.cs (2)
118-130: Verify FontMetrics signs/valuesAscent = -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) methodThe reviewed method at lines 76β85 is never called in the codebase. Text shaping uses the string-based
GetGlyphsextension method (from UtilityExtensions.cs), which properly clusters emoji and variation sequences. The active flow in TextShaper.cs then callsGetGlyphIndex(string)on each clustered glyph, preserving widths correctly. Either delete this unused method or confirm it's retained for API compatibility.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 markerApply 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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| public bool TryGetTable(uint tag, out byte[] table) | ||
| { | ||
| throw new NotImplementedException(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© 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 3Length 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 -60Length 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=csLength 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 fLength 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
fiLength 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.
| 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.
There was a problem hiding this 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
iis declared asushort, which will wrap at 65,535. IfshapedBuffer.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
breakstatement at line 102 exits the entirewhileloop, truncating all remaining text in the input string. This causes text after the joiner to be silently dropped.The past review comment recommended using
continueinstead, but that's also problematic because it would leave the joiner inlastRune. Better to skip the joiner without updatinglastRune: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
lastRuneandindexupdates 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.DiagnosticsandSystem.Linqare 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
Graphemeclass properties are currently mutable. Since graphemes are created once and not modified, consider usinginitaccessors 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cssrc/Consolonia.Core/Helpers/UtilityExtensions.cssrc/Consolonia.Core/Text/TextShaper.cssrc/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
falseprovide a clean pattern for detecting and exposing terminal capabilities. This allows the detection to run duringPrepareConsolewhile 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
bufferPointand set_headBufferPointat 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:
- Tests complex emoji support by writing a family emoji and checking if cursor advances by 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:
- The current emoji buffer if one is being built
- 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
regionalRuneCountwhen 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
regionalRuneCounthere 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
Textcontent andClusterindices, ensuring correct grapheme segmentation and layout positioning.
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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
|
PR diff hash: 0e6fcfe2ce0be544d87186fd1e42274a69535f01749ea992acf414e18237a377 |
There was a problem hiding this 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 dataCurrent 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 voidThe 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Helpers/Grapheme.cssrc/Consolonia.Core/Text/TextShaper.cssrc/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cssrc/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 emojiOn 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.
| if (glyphWidth > 1) | ||
| { | ||
| WriteText(Esc.SetCursorPosition(bufferPoint.X + 1, bufferPoint.Y)); | ||
| WriteText(new string(' ', Math.Min(Size.Width - bufferPoint.X - 1, glyphWidth - 1))); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| public void GetgraphemesEmptyStringReturnsEmptyList() | ||
| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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.
ushortwraps at 65535; can hang forLength >= 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 whenbufferPoint.X >= Size.Width. This will throw innew 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
π 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; rundotnet formatso CI passes formatting checks
Fix IDE1006 naming rule violations manually (dotnet format will not auto-fix these)
Files:
src/Consolonia.Core/Helpers/Grapheme.cssrc/Consolonia.Core/Text/TextShaper.cssrc/Consolonia.Core/Infrastructure/AnsiConsoleOutput.cssrc/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.
| /// 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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
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
Effects
This has the effect of:
Icons before changes, showing how chars overlap with following character:

Icons after changes:
