Merge pull request #25 from HueByte/dev_improve_word_wrapping

feat: improve word wrapping in ChatLine
This commit is contained in:
Stone_Red
2026-02-24 13:54:36 +01:00
committed by GitHub
3 changed files with 70 additions and 37 deletions
+60 -30
View File
@@ -1,3 +1,4 @@
using System.Text;
using System.Text.RegularExpressions;
using EchoHub.Core.Models;
using Terminal.Gui.Drawing;
@@ -18,6 +19,8 @@ public partial class ChatLine
public string? AttachmentUrl { get; set; }
public string? AttachmentFileName { get; set; }
public MessageType? Type { get; set; }
/// <summary>Number of spaces to prepend on continuation lines when this line is word-wrapped.</summary>
public int ContinuationIndent { get; set; }
public ChatLine(string plainText)
{
@@ -42,51 +45,78 @@ public partial class ChatLine
if (width <= 0 || TextLength <= width)
return [this];
var results = new List<ChatLine>();
var currentSegments = new List<ChatSegment>();
int col = 0;
var tokens = new List<(string grapheme, Attribute? color)>();
foreach (var segment in Segments)
{
var text = segment.Text;
int chunkStart = 0;
int charPos = 0;
foreach (var g in GraphemeHelper.GetGraphemes(segment.Text))
tokens.Add((g, segment.Color));
foreach (var grapheme in GraphemeHelper.GetGraphemes(text))
{
var graphemeCols = Math.Max(grapheme.GetColumns(), 1);
var results = new List<ChatLine>();
int pos = 0;
bool firstLine = true;
if (col + graphemeCols > width)
while (pos < tokens.Count)
{
if (charPos > chunkStart)
currentSegments.Add(new ChatSegment(text[chunkStart..charPos], segment.Color));
// First line uses the full width
int col = firstLine ? 0 : continuationIndent;
int lastSpaceIdx = -1;
results.Add(new ChatLine(currentSegments));
currentSegments = [];
if (continuationIndent > 0)
int i = pos;
for (; i < tokens.Count; i++)
{
currentSegments.Add(new ChatSegment(new string(' ', continuationIndent), null));
col = continuationIndent;
var graphemeCols = Math.Max(tokens[i].grapheme.GetColumns(), 1);
if (col + graphemeCols > width) break;
if (tokens[i].grapheme == " ") lastSpaceIdx = i;
col += graphemeCols;
}
int lineEnd, nextPos;
if (i == tokens.Count)
{
// All remaining tokens fit on this line.
lineEnd = tokens.Count;
nextPos = tokens.Count;
}
else if (lastSpaceIdx >= pos)
{
// Break at the last space that fit, skip the space itself.
lineEnd = lastSpaceIdx;
nextPos = lastSpaceIdx + 1;
}
else
{
col = 0;
// No space found, break word.
lineEnd = Math.Max(i, pos + 1);
nextPos = lineEnd;
}
chunkStart = charPos;
var segments = new List<ChatSegment>();
if (!firstLine && continuationIndent > 0)
segments.Add(new ChatSegment(new string(' ', continuationIndent), null));
// Rebuild segments by grouping consecutive same-color tokens.
var sb = new StringBuilder();
int groupStart = pos;
while (groupStart < lineEnd)
{
var color = tokens[groupStart].color;
sb.Clear();
int groupEnd = groupStart;
while (groupEnd < lineEnd && tokens[groupEnd].color == color)
{
sb.Append(tokens[groupEnd].grapheme);
groupEnd++;
}
segments.Add(new ChatSegment(sb.ToString(), color));
groupStart = groupEnd;
}
col += graphemeCols;
charPos += grapheme.Length;
results.Add(new ChatLine(segments));
pos = nextPos;
firstLine = false;
}
if (chunkStart < text.Length)
currentSegments.Add(new ChatSegment(text[chunkStart..], segment.Color));
}
if (currentSegments.Count > 0)
results.Add(new ChatLine(currentSegments));
if (results.Count == 0)
return [this];
// Propagate attachment/type metadata to all wrapped lines so they remain clickable
foreach (var wrapped in results)
@@ -259,6 +259,9 @@ public sealed class ChatMessageManager
lines.Add(new ChatLine(ChatColors.SplitMentions(contText)));
}
foreach (var l in lines)
l.ContinuationIndent = indent.Length;
if (message.Embeds is { Count: > 0 })
{
var chatWidth = _chatWidth > 0 ? _chatWidth : 80;
+1 -1
View File
@@ -772,7 +772,7 @@ public sealed class MainWindow : Runnable
if (width > 0)
{
foreach (var line in messages)
source.AddRange(line.Wrap(width));
source.AddRange(line.Wrap(width, line.ContinuationIndent));
}
else
{