< Summary

Line coverage
93%
Covered lines: 135
Uncovered lines: 10
Coverable lines: 145
Total lines: 342
Line coverage: 93.1%
Branch coverage
88%
Covered branches: 104
Total branches: 118
Branch coverage: 88.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: FunctionRegex()100%11100%
File 2: .ctor(...)100%11100%
File 2: UpdateSegments(...)86.36%454492%
File 2: FindOpeningTag(...)60%101083.33%
File 2: AppendToken(...)75%131283.33%
File 2: ProcessIncomingText(...)100%88100%
File 2: Close(...)83.33%7675%
File 2: Parse(...)100%3838100%

File(s)

/home/runner/work/InvAit/InvAit/UIBlazor/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/InvAit/InvAit/UIBlazor/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/InvAit/InvAit/UIBlazor/Services/MessageParser.cs

#LineLine coverage
 1using UIBlazor.Services.Settings;
 2
 3namespace UIBlazor.Services;
 4
 175public partial class MessageParser(IToolManager toolManager) : IMessageParser
 6{
 7    public void UpdateSegments(string delta, VisualChatMessage message, bool isHistory = false)
 8    {
 579        if (string.IsNullOrEmpty(delta))
 110            return;
 11
 10212        var activeSegment = isHistory ? null : message.Segments.LastOrDefault(s => !s.IsClosed);
 13
 5614        if (isHistory)
 15        {
 16            // В истории дельта - это целое неизменное сообщение.
 17            // Поэтому активный сегмент всегда новый для каждого вызова.
 018            activeSegment = null;
 19        }
 20
 5621        var incomingText = delta;
 22
 12323        while (!string.IsNullOrEmpty(incomingText))
 24        {
 25            // Логика сегментов
 6726            if (activeSegment == null || activeSegment.IsClosed)
 27            {
 1528                _rawAccumulator.Clear();
 1529                activeSegment = new ContentSegment();
 1530                message.Segments.Add(activeSegment);
 31            }
 32
 6733            var openIdx = FindOpeningTag(incomingText);
 6734            var closeIdx = activeSegment.Type is not SegmentType.Unknown and not SegmentType.Markdown
 6735                           ? incomingText.IndexOf($"</{activeSegment.TagName}>", StringComparison.Ordinal)
 6736                           : -1;
 37
 38            // Сценарий А: Закрытие
 6739            if (closeIdx != -1 && (openIdx == -1 || closeIdx < openIdx))
 40            {
 941                var closingTag = $"</{activeSegment.TagName}>";
 942                var endOfTag = closeIdx + closingTag.Length;
 943                AppendToken(activeSegment, incomingText[..endOfTag]);
 944                Close(activeSegment);
 945                incomingText = incomingText[endOfTag..];
 946                continue;
 47            }
 48
 49            // Сценарий Б: Открытие
 5850            if (openIdx != -1)
 51            {
 1352                if (openIdx > 0)
 53                {
 154                    AppendToken(activeSegment, incomingText[..openIdx]);
 155                    Close(activeSegment);
 156                    incomingText = incomingText[openIdx..];
 157                    continue;
 58                }
 59
 1260                if (activeSegment is { IsClosed: false, Type: SegmentType.Markdown })
 61                {
 262                    Close(activeSegment);
 263                    continue;
 64                }
 65
 66                // Находим конец тега '>', чтобы знать, где кончаются параметры (name="...")
 1067                var tagEndIdx = incomingText.IndexOf('>');
 1068                if (tagEndIdx != -1)
 69                {
 1070                    var consumptionLength = tagEndIdx + 1;
 1071                    AppendToken(activeSegment, incomingText[..consumptionLength]);
 1072                    if (activeSegment.Type == SegmentType.Tool && !string.IsNullOrEmpty(activeSegment.ToolName))
 73                    {
 974                        if (isHistory) // При загрузке истории все тулзы заапрувлены. Чтобы не было ложного Pending.
 75                        {
 076                            activeSegment.ApprovalStatus = ToolApprovalStatus.Approved;
 77                        }
 78                        else
 79                        {
 80                            // ну и сразу ставим статус, чтобы не было гонки между рендером и обновлением статуса после 
 981                            var mode = toolManager.GetApprovalModeByToolName(activeSegment.ToolName);
 982                            activeSegment.ApprovalStatus = mode switch
 983                            {
 084                                ToolApprovalMode.Ask => ToolApprovalStatus.Pending,
 085                                ToolApprovalMode.Deny => ToolApprovalStatus.Rejected,
 986                                _ => ToolApprovalStatus.Approved
 987                            };
 88                        }
 89                    }
 1090                    incomingText = incomingText[consumptionLength..];
 1091                    continue;
 92                }
 93            }
 94
 95            // Сценарий В: Обычный контент
 4596            AppendToken(activeSegment, incomingText);
 4597            incomingText = string.Empty;
 98        }
 5699    }
 100
 101    private static int FindOpeningTag(string text)
 102    {
 67103        var planIdx = text.IndexOf("<plan", StringComparison.Ordinal);
 67104        var funcIdx = text.IndexOf("<function", StringComparison.Ordinal);
 105
 121106        if (planIdx == -1 && funcIdx == -1) return -1;
 107
 108        // Берем тот, что встретился раньше
 13109        if (planIdx != -1 && (funcIdx == -1 || planIdx < funcIdx))
 110        {
 0111            return planIdx;
 112        }
 13113        return funcIdx;
 114    }
 115
 116    // Вспомогательный буфер для накопления "сырого" текста внутри сегмента
 17117    private readonly StringBuilder _rawAccumulator = new();
 118
 119    private void AppendToken(ContentSegment segment, string token)
 120    {
 65121        if (segment.IsClosed || string.IsNullOrEmpty(token)) return;
 122
 65123        _rawAccumulator.Append(token);
 124
 125        // 1. Определяем тип, если он еще не известен. Теги приходят полными
 65126        if (segment.Type == SegmentType.Unknown)
 127        {
 15128            var raw = _rawAccumulator.ToString();
 15129            var functionMatch = FunctionRegex().Match(token);
 15130            if (functionMatch.Success)
 131            {
 9132                segment.Type = SegmentType.Tool;
 9133                segment.TagName = "function";
 9134                segment.ToolName = functionMatch.Groups["name"].Value;
 9135                return;
 136            }
 137
 6138            if (raw.Contains("<plan>"))
 139            {
 0140                segment.Type = SegmentType.Plan;
 0141                segment.TagName = "plan";
 0142                return;
 143            }
 144
 6145            if (!string.IsNullOrEmpty(raw))
 146            {
 6147                segment.Type = SegmentType.Markdown;
 148            }
 149        }
 150
 151        // 2. Обрабатываем текст и разбиваем на линии
 56152        ProcessIncomingText(segment, token);
 56153    }
 154
 155    private static void ProcessIncomingText(ContentSegment segment, string token)
 156    {
 56157        segment.CurrentLine.Append(token);
 158
 159        // Если есть перенос строки - фиксируем завершенные линии
 56160        if (segment.Type != SegmentType.Markdown && token.Contains('\n'))
 161        {
 22162            var content = segment.CurrentLine.ToString();
 22163            var parts = content.Split('\n');
 164
 22165            if (!string.IsNullOrWhiteSpace(parts[0]))
 166            {
 13167                segment.Lines.Add(parts[0]);
 168            }
 169
 72170            for (var i = 1; i < parts.Length - 1; i++)
 171            {
 14172                segment.Lines.Add(parts[i]);
 173            }
 174
 22175            segment.ToolParams = Parse(segment.ToolName, segment.Lines);
 176
 22177            segment.CurrentLine.Clear();
 22178            segment.CurrentLine.Append(parts.Last());
 179        }
 56180    }
 181
 182    private static void Close(ContentSegment segment)
 183    {
 12184        segment.IsClosed = true;
 185        // Переносим остаток из буфера в финальные линии, если он там есть
 12186        if (segment.Type != SegmentType.Markdown && segment.CurrentLine.Length > 0)
 187        {
 9188            var lastLine = segment.CurrentLine.ToString();
 189            // только если последняя линия - не закрывающий тег (99,9% случаев)
 9190            if (!lastLine.Trim().Equals($"</{segment.TagName}>", StringComparison.OrdinalIgnoreCase))
 191            {
 0192                segment.Lines.Add(lastLine.Replace($"</{segment.TagName}>", ""));
 0193                segment.ToolParams = Parse(segment.ToolName, segment.Lines);
 194            }
 9195            segment.CurrentLine.Clear();
 196        }
 12197    }
 198
 199    /// <summary>
 200    /// Парсим рагументы тулзы перед вызовом
 201    /// </summary>
 202    /// <param name="toolName">Имя тулзы</param>
 203    /// <param name="toolLines">Параметры по линиям</param>
 204    public static Dictionary<string, object> Parse(string toolName, List<string> toolLines, bool isClosed = false)
 205    {
 29206        var result = new Dictionary<string, object>();
 29207        var paramIndex = 0;
 29208        var namedIndex = 0;
 209
 210        switch (toolName)
 211        {
 212            case BuiltInToolEnum.ReadFiles:
 213                {
 34214                    for (var i = 0; i < toolLines.Count; i++)
 215                    {
 10216                        var line = toolLines[i].Trim();
 217
 10218                        if (string.IsNullOrEmpty(line))
 219                            continue;
 220
 10221                        var match = Regex.Match(line, @"^(?<path>.*?)(?:\s*\[L(?<line>\d+)(?::C(?<count>\d+))?\])?$", Re
 10222                        if (match.Success)
 223                        {
 10224                            var fileParams = new ReadFileParams
 10225                            {
 10226                                Name = match.Groups["path"].Value,
 10227                                StartLine = match.Groups["line"].Success && int.TryParse(match.Groups["line"].Value, out
 10228                                LineCount = match.Groups["count"].Success && int.TryParse(match.Groups["count"].Value, o
 10229                            };
 10230                            result[$"file{++paramIndex}"] = fileParams;
 231                        }
 232                    }
 233
 7234                    break;
 235                }
 236            case BuiltInToolEnum.ApplyDiff:
 237                {
 106238                    for (var i = 0; i < toolLines.Count; i++)
 239                    {
 36240                        var line = toolLines[i].Trim();
 241
 36242                        if (string.IsNullOrEmpty(line))
 243                            continue;
 244
 245                        // Начало блока (<<<<<<< SEARCH)
 36246                        if (line.StartsWith("<<<<<<< SEARCH"))
 247                        {
 13248                            i++;
 13249                            var diff = new DiffReplacement();
 13250                            var options = line[14..].TrimStart();
 13251                            if (options.StartsWith(':'))
 252                            {
 12253                                diff.StartLine = int.Parse(options.Split(':')[1]);
 254                            }
 13255                            var search = new List<string>();
 39256                            for (; i < toolLines.Count; i++)
 257                            {
 22258                                line = toolLines[i].TrimEnd();
 22259                                if (line.StartsWith("======="))
 260                                {
 9261                                    i++;
 9262                                    break;
 263                                }
 13264                                else if (line.StartsWith(">>>>>>> REPLACE")) // если нужно просто удалить, то могут упус
 265                                {
 266                                    break;
 267                                }
 13268                                search.Add(line);
 269                            }
 13270                            diff.Search = search;
 271
 13272                            var replace = new List<string>();
 33273                            for (; i < toolLines.Count; i++)
 274                            {
 16275                                line = toolLines[i].TrimEnd();
 16276                                if (line.StartsWith(">>>>>>> REPLACE"))
 277                                {
 6278                                    i++;
 6279                                    break;
 280                                }
 10281                                replace.Add(line);
 282                            }
 13283                            diff.Replace = replace;
 284
 13285                            result[$"diff{++namedIndex}"] = diff;
 286                        }
 287                        // Обычная строка параметров
 288                        else
 289                        {
 23290                            result[$"param{++paramIndex}"] = line;
 291                        }
 292                    }
 293
 17294                    break;
 295                }
 296            default:
 297                {
 48298                    foreach (var line in toolLines)
 299                    {
 19300                        result[$"param{++paramIndex}"] = line;
 301                    }
 302                    break;
 303                }
 304        }
 305
 29306        return result;
 307    }
 308
 309    [GeneratedRegex(@"<function name( {0,1})=( {0,1})""(?<name>[\w-_\.]+)"">$", RegexOptions.Compiled | RegexOptions.Non
 310    private static partial Regex FunctionRegex();
 311}