File: Commands\PackageSearch\Table.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.CommandLine.XPlat\NuGet.CommandLine.XPlat.csproj (NuGet.CommandLine.XPlat)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace NuGet.CommandLine.XPlat
{
    internal class Column
    {
        public string Header { get; set; }
        public int Width { get; set; }
        public bool Highlight { get; set; }
    }

    internal class Table
    {
        // This is the default window width if we cannot get the actual window width
        internal const int DefaultWindowWidth = 115;
        // This is the minimum number of characters in a column which includes "| c |" where c is a character
        const int MinimumCharactersInAColumn = 4;
        // This is the list of columns in the table
        internal readonly List<Column> _columns = new List<Column>();
        // This is the list of rows in the table
        internal List<string[]> _rows = new List<string[]>();
        // This is the list of columns to highlight
        private int[] _columnsToHighlight;
        // This is the highlighter color
        private ConsoleColor _highlighter = ConsoleColor.Red;
        // This is the maximum column width: The maximum number of characters in a column based on the window width
        private readonly int _maxColumnWidth;
        // This is the default console color
        private readonly ConsoleColor _consoleColor = Console.ForegroundColor;

        public Table(int[] columnsToHighlight, string[] headers, int consoleWidth)
        {
            if (consoleWidth <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(consoleWidth), consoleWidth, "Console width must be greater than zero.");
            }

            _columnsToHighlight = columnsToHighlight;

            _maxColumnWidth = Math.Max(MinimumCharactersInAColumn, (consoleWidth - MinimumCharactersInAColumn * headers.Length) / headers.Length);

            // Add the headers
            foreach (var header in headers)
            {
                _columns.Add(new Column { Header = header, Width = header.Length });
            }
        }

        /* Add a row to the table
         * row: The list of values in the row
         */
        public void AddRow(params string[] row)
        {
            if (row.Length != _columns.Count)
            {
                throw new InvalidOperationException("Row column count does not match header column count.");
            }

            for (int i = 0; i < row.Length; i++)
            {
                _columns[i].Width = Math.Min(_maxColumnWidth, Math.Max(_columns[i].Width, row[i]?.Length ?? 0));
            }

            _rows.Add(row);
        }

        /* Print the table with highlighting
         * logger: The logger to use for printing
         * highlightTerm: The term to highlight in the table
         */
        public void PrintResult(string highlightTerm, ILoggerWithColor logger)
        {
            if (_rows.Count == 0)
            {
                logger.LogMinimal("No results found.");
                return;
            }
            // Print the header
            PrintRow(logger, _columns.Select(c => c.Header).ToList(), highlightTerm);
            // Print a separator line
            PrintRow(logger, _columns.Select(c => "".PadRight(c.Width, '-')).ToList(), "");

            foreach (string[] row in _rows)
            {
                // Sanitize the values to remove new lines and tabs
                List<string> sanitizedValues = row.Select(v => SanitizeString(v)).ToList();
                PrintRow(logger, sanitizedValues, highlightTerm);

                // Print a separator line
                PrintRow(logger, _columns.Select(c => "".PadRight(c.Width, '-')).ToList(), "");
            }
        }

        private string SanitizeString(string value)
        {
            return Regex.Replace(value ?? string.Empty, @"\r\n|\n\r|\n|\r|\t", " ");
        }

        /* Print a row in the table
         * logger: The logger to use for printing
         * values: The list of values in the row
         * highlightTerm: The term to highlight in the row
         */
        private void PrintRow(ILoggerWithColor logger, List<string> values, string highlightTerm)
        {
            ConsoleColor color = _consoleColor;

            // In one row there could be multiple rows if the value is too long. subRow is the index of the sub row
            int subRow = 0;
            // Keep track of the columns that have been printed
            List<int> renderedColumns = new List<int>();
            bool done = false;

            List<List<int>> highlight = new List<List<int>>();

            // Find the indices of the highlight term in each value
            foreach (string value in values)
            {
                highlight.Add(FindSubstringIndices(value, highlightTerm));
            }

            // Keep printing the row until all the columns have been printed
            while (!done)
            {
                // Print column by column
                for (int column = 0; column < _columns.Count; column++)
                {
                    logger.LogInline("| ", color);
                    string value = values[column];

                    // For each column, print character by character with the appropriate color
                    for (int i = 0; i < _columns[column].Width; i++)
                    {
                        int CharacterIndex = subRow * _columns[column].Width + i;

                        // Change to highlighter color if the character index is within the highlight term
                        if (_columnsToHighlight.Contains(column) && highlight[column].Contains(CharacterIndex))
                        {
                            color = _highlighter;
                        }

                        // All the characters have been printed
                        if (CharacterIndex >= value.Length)
                        {
                            if (!renderedColumns.Contains(column))
                            {
                                renderedColumns.Add(column);
                            }

                            logger.LogInline("".PadRight(_columns[column].Width - i), color);
                            break;
                        }

                        // If the character index is within the length of the value, print the character
                        if (CharacterIndex < value.Length)
                        {
                            logger.LogInline(value[CharacterIndex].ToString(), color);
                        }
                        else
                        {
                            logger.LogInline(" ", color);
                        }

                        // If the character index is the last character in the value, add the column to the list of rendered columns
                        if (CharacterIndex == value.Length - 1)
                        {
                            if (!renderedColumns.Contains(column))
                            {
                                renderedColumns.Add(column);
                            }
                        }

                        // Reset the color to the default color
                        color = _consoleColor;
                    }

                    logger.LogInline(" ", color);
                }

                // New line for new row
                logger.LogInline("|", color);
                logger.LogMinimal("");
                subRow++;

                // If all the columns have been printed, we are done
                if (renderedColumns.Count >= values.Count)
                {
                    done = true;
                }
            }
        }

        private static List<int> FindSubstringIndices(string str, string substring)
        {
            List<int> indices = new List<int>();

            if (string.IsNullOrEmpty(substring))
            {
                return indices;
            }

            int index = 0;
            while ((index = str.IndexOf(substring, index, StringComparison.CurrentCultureIgnoreCase)) != -1)
            {
                for (int i = 0; i < substring.Length; i++)
                {
                    indices.Add(index + i);
                }
                index += substring.Length;
            }

            return indices;
        }
    }
}