spectre.console icon indicating copy to clipboard operation
spectre.console copied to clipboard

`Paragraph.SplitLines` fails to split a string if it contains Japanese characters

Open Tyrrrz opened this issue 8 months ago • 2 comments

Information

  • OS: Windows 11
  • Version: 0.49.1
  • Terminal: Windows Terminal (also see additional context)

Describe the bug

I'm using Spectre.Console.Progress to report progress on several parallel tasks:

https://github.com/Tyrrrz/DiscordChatExporter/blob/09e0b3f1334bcbe413c5e577f3e812fb19a0f62b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs#L192-L245

It appears that, when a new task is started with the following description, Spectre.Console throws an exception:

神様達が下界に来る前は、魔法は特定の種族の専売特許に過ぎなかった。けれど、神様達の『恩恵』はいかなる者でも魔法を発現させることを可能としたのだ。

Exception:

System.ArgumentOutOfRangeException: Index and length must refer to a location within the string. (Parameter 'length')
   at System.String.ThrowSubstringArgumentOutOfRange(Int32 startIndex, Int32 length)
   at System.String.Substring(Int32 startIndex, Int32 length)
   at Spectre.Console.Rendering.Segment.SplitOverflow(Segment segment, Nullable`1 overflow, Int32 maxWidth) in /_/src/Spectre.Console/Rendering/Segment.cs:line 371
   at Spectre.Console.Paragraph.SplitLines(Int32 maxWidth) in /_/src/Spectre.Console/Widgets/Paragraph.cs:line 236
   at Spectre.Console.Paragraph.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Widgets/Paragraph.cs:line 141
   at Spectre.Console.Rendering.Renderable.Spectre.Console.Rendering.IRenderable.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Rendering/Renderable.cs:line 19
   at Spectre.Console.Markup.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Widgets/Markup.cs:line 54
   at Spectre.Console.Rendering.Renderable.Spectre.Console.Rendering.IRenderable.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Rendering/Renderable.cs:line 19
   at Spectre.Console.TableRenderer.Render(TableRendererContext context, List`1 columnWidths) in /_/src/Spectre.Console/Widgets/Table/TableRenderer.cs:line 31
   at Spectre.Console.Table.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Widgets/Table/Table.cs:line 147
   at Spectre.Console.Rendering.Renderable.Spectre.Console.Rendering.IRenderable.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Rendering/Renderable.cs:line 19
   at Spectre.Console.Rendering.JustInTimeRenderable.Render(RenderOptions context, Int32 width) in /_/src/Spectre.Console/Rendering/JustInTimeRenderable.cs:line 21
   at Spectre.Console.Rendering.Renderable.Spectre.Console.Rendering.IRenderable.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Rendering/Renderable.cs:line 19
   at Spectre.Console.TableRenderer.Render(TableRendererContext context, List`1 columnWidths) in /_/src/Spectre.Console/Widgets/Table/TableRenderer.cs:line 31
   at Spectre.Console.Table.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Widgets/Table/Table.cs:line 147
   at Spectre.Console.Rendering.Renderable.Spectre.Console.Rendering.IRenderable.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Rendering/Renderable.cs:line 19
   at Spectre.Console.Rendering.JustInTimeRenderable.Render(RenderOptions context, Int32 width) in /_/src/Spectre.Console/Rendering/JustInTimeRenderable.cs:line 21
   at Spectre.Console.Rendering.Renderable.Spectre.Console.Rendering.IRenderable.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Rendering/Renderable.cs:line 19
   at Spectre.Console.Padder.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Widgets/Padder.cs:line 69
   at Spectre.Console.Rendering.Renderable.Spectre.Console.Rendering.IRenderable.Render(RenderOptions options, Int32 maxWidth) in /_/src/Spectre.Console/Rendering/Renderable.cs:line 19
   at Spectre.Console.Rendering.LiveRenderable.Render(RenderOptions options, Int32 maxWidth)+MoveNext() in /_/src/Spectre.Console/Live/LiveRenderable.cs:line 78
   at System.Collections.Generic.List`1.AddRange(IEnumerable`1 collection)
   at Spectre.Console.RenderableExtensions.GetSegments(IAnsiConsole console, RenderOptions options, IEnumerable`1 renderables) in /_/src/Spectre.Console/Extensions/RenderableExtensions.cs:line 37
   at Spectre.Console.RenderableExtensions.GetSegments(IRenderable renderable, IAnsiConsole console) in /_/src/Spectre.Console/Extensions/RenderableExtensions.cs:line 29
   at Spectre.Console.AnsiBuilder.Build(IAnsiConsole console, IRenderable renderable) in /_/src/Spectre.Console/Internal/Backends/Ansi/AnsiBuilder.cs:line 17
   at Spectre.Console.AnsiConsoleBackend.Write(IRenderable renderable) in /_/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs:line 30
   at Spectre.Console.AnsiConsoleFacade.Write(IRenderable renderable) in /_/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs:line 40
   at Spectre.Console.ProgressContext.Refresh() in /_/src/Spectre.Console/Live/Progress/ProgressContext.cs:line 178
   at Spectre.Console.ProgressRefreshThread.Run() in /_/src/Spectre.Console/Live/Progress/ProgressRefreshThread.cs:line 45

I'm able to debug the callstack a bit and these are the last few frames that could be of interest:

Image

Image

Image

To Reproduce

I've decomposed the issue to the following simplest reproduction sample:

Segment.SplitOverflow(
            new Segment(
                "神様達が下界に来る前は、魔法は特定の種族の専売特許に過ぎなかった。けれど、神様達の『恩恵』はいかなる者でも魔法を発現させることを可能としたのだ。"
            ),
            Overflow.Ellipsis,
            118
        );

Expected behavior

There should be no exception.

Screenshots

See above.

Additional context

I've noticed that the issue is not reproduced in the JetBrains Rider's integrated terminal, where the Japanese characters are replaced by ?. However, that might be unrelated.

Original issue: https://github.com/Tyrrrz/DiscordChatExporter/issues/1354


Please upvote :+1: this issue if you are interested in it.

Tyrrrz avatar Mar 30 '25 16:03 Tyrrrz

I had a look at this and I think I'm also able to reproduce the issue on Mac in Rider, as well as the default Terminal, using the following:

AnsiConsole.Write(new Text(new string('あ', 200)) { Overflow = Overflow.Ellipsis });

[!NOTE] If I just click Run in Rider, it doesn't output anything, doesn't terminate and it starts eating up memory according to the Monitoring tab 😅. However, if I click Debug it does stop at the Exception, so I'm guessing this is probably a Rider bug and unrelated.

Anyway, the Japanese text uses fullwidth characters that, in a monospaced font, are twice as wide as regular alphanumeric characters.

In the Segment class, the logic for splitting/truncating is all based on the width in terms of terminal columns. However, in our particular method, we're using this width as an argument for .Substring(), when it's expecting the substring length - which in this case is less than the width - hence the OutOfRange exception.

As an example:

Image

Proposed fix

  • We can build up the substring whilst counting the width - this is already done similarly in SplitSegment() and Truncate():

https://github.com/spectreconsole/spectre.console/blob/d836ad18057fff49c119da310c6d86672fe38f48/src/Spectre.Console/Rendering/Segment.cs#L593-L604

  • We can also add coverage in https://github.com/spectreconsole/spectre.console/blob/main/src/Tests/Spectre.Console.Tests/Unit/Rendering/SegmentTests.cs It already has the relevant test cases for Chinese characters, but only for Split() as far as I can tell.

@patriksvensson Is it alright if I raise a PR with a fix?

dangerman avatar May 16 '25 18:05 dangerman

@dangerman Of course. Bug fixes are always welcome 😄

patriksvensson avatar May 16 '25 18:05 patriksvensson