File: TerminalLogger\NodesFrame.cs
Web Access
Project: ..\..\..\src\MSBuild\MSBuild.csproj (MSBuild)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
 
using System.Text;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Logging.TerminalLogger;
 
/// <summary>
/// Capture states on nodes to be rendered on display.
/// </summary>
internal sealed class NodesFrame
{
    private const int MaxColumn = 120;
 
    private readonly (NodeStatus nodeStatus, int durationLength)[] _nodes;
 
    private readonly StringBuilder _renderBuilder = new();
 
    public int Width { get; }
    public int Height { get; }
    public int NodesCount { get; private set; }
 
    public NodesFrame(NodeStatus?[] nodes, int width, int height)
    {
        Width = Math.Min(width, MaxColumn);
        Height = height;
 
        _nodes = new (NodeStatus, int)[nodes.Length];
 
        foreach (NodeStatus? status in nodes)
        {
            if (status is not null)
            {
                _nodes[NodesCount++].nodeStatus = status;
            }
        }
    }
 
    internal ReadOnlySpan<char> RenderNodeStatus(int i)
    {
        NodeStatus status = _nodes[i].nodeStatus;
 
        string durationString = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(
            "DurationDisplay",
            status.Stopwatch.ElapsedSeconds);
 
        _nodes[i].durationLength = durationString.Length;
 
        string project = status.Project;
        string? targetFramework = status.TargetFramework;
        string target = status.Target;
        string? targetPrefix = status.TargetPrefix;
        TerminalColor targetPrefixColor = status.TargetPrefixColor;
 
        var targetWithoutAnsiLength = !string.IsNullOrWhiteSpace(targetPrefix)
            // +1 because we will join them by space in the final output.
            ? targetPrefix!.Length + 1 + target.Length
            : target.Length;
 
        int renderedWidth = Length(durationString, project, targetFramework, targetWithoutAnsiLength);
 
        if (renderedWidth > Width)
        {
            renderedWidth -= targetWithoutAnsiLength;
            targetPrefix = target = string.Empty;
            targetWithoutAnsiLength = 0;
 
            if (renderedWidth > Width)
            {
                int lastDotInProject = project.LastIndexOf('.');
                renderedWidth -= lastDotInProject;
                project = project.Substring(lastDotInProject + 1);
 
                if (renderedWidth > Width)
                {
                    return project.AsSpan();
                }
            }
        }
 
        var renderedTarget = !string.IsNullOrWhiteSpace(targetPrefix) ? $"{AnsiCodes.Colorize(targetPrefix, targetPrefixColor)} {target}" : target;
        return $"{TerminalLogger.Indentation}{project}{(targetFramework is null ? string.Empty : " ")}{AnsiCodes.Colorize(targetFramework, TerminalLogger.TargetFrameworkColor)} {AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(targetWithoutAnsiLength + durationString.Length + 1)}{renderedTarget} {durationString}".AsSpan();
 
        static int Length(string durationString, string project, string? targetFramework, int targetWithoutAnsiLength) =>
                TerminalLogger.Indentation.Length +
                project.Length + 1 +
                (targetFramework?.Length ?? -1) + 1 +
                targetWithoutAnsiLength + 1 +
                durationString.Length;
    }
 
    /// <summary>
    /// Render VT100 string to update from current to next frame.
    /// </summary>
    public string Render(NodesFrame previousFrame)
    {
        StringBuilder sb = _renderBuilder;
        sb.Clear();
 
        // Move cursor back to 1st line of nodes.
        sb.AppendLine($"{AnsiCodes.CSI}{previousFrame.NodesCount + 1}{AnsiCodes.MoveUpToLineStart}");
 
        int i = 0;
        for (; i < NodesCount; i++)
        {
            ReadOnlySpan<char> needed = RenderNodeStatus(i);
 
            // Do we have previous node string to compare with?
            if (previousFrame.NodesCount > i)
            {
                if (previousFrame._nodes[i] == _nodes[i])
                {
                    // Same everything except time, AND same number of digits in time
                    string durationString = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("DurationDisplay", _nodes[i].nodeStatus.Stopwatch.ElapsedSeconds);
                    sb.Append($"{AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(durationString.Length)}{durationString}");
                }
                else
                {
                    // TODO: check components to figure out skips and optimize this
                    sb.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}");
                    sb.Append(needed);
                }
            }
            else
            {
                // From now on we have to simply WriteLine
                sb.Append(needed);
            }
 
            // Next line
            sb.AppendLine();
        }
 
        // clear no longer used lines
        if (i < previousFrame.NodesCount)
        {
            sb.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInDisplay}");
        }
 
        return sb.ToString();
    }
 
    public void Clear()
    {
        NodesCount = 0;
    }
}