Skip to content

Commit 7e5f62c

Browse files
Implement a dynamically wrapping table for dotnet package search (#5683)
1 parent 8c972cd commit 7e5f62c

5 files changed

Lines changed: 326 additions & 206 deletions

File tree

src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageSearch/Table.cs

Lines changed: 164 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,119 +4,232 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Text.RegularExpressions;
78

89
namespace NuGet.CommandLine.XPlat
910
{
11+
internal class Column
12+
{
13+
public string Header { get; set; }
14+
public int Width { get; set; }
15+
public bool Highlight { get; set; }
16+
}
17+
1018
internal class Table
1119
{
20+
// This is the default window width if we cannot get the actual window width
21+
const int DefaultWindowWidth = 115;
22+
// This is the minimum number of characters in a column which includes "| c |" where c is a character
23+
const int MinimumCharactersInAColumn = 4;
24+
// This is the list of columns in the table
25+
internal readonly List<Column> _columns = new List<Column>();
26+
// This is the list of rows in the table
1227
internal List<string[]> _rows = new List<string[]>();
13-
private int[] _columnWidths;
28+
// This is the list of columns to highlight
1429
private int[] _columnsToHighlight;
30+
// This is the highlighter color
31+
private ConsoleColor _highlighter = ConsoleColor.Red;
32+
// This is the maximum column width: The maximum number of characters in a column based on the window width
33+
private readonly int _maxColumnWidth;
34+
// This is the default console color
35+
private readonly ConsoleColor _consoleColor = Console.ForegroundColor;
1536

1637
public Table(int[] columnsToHighlight, params string[] headers)
1738
{
1839
_columnsToHighlight = columnsToHighlight;
19-
_columnWidths = new int[headers.Length];
40+
int windowWidth = -1;
2041

21-
for (int i = 0; i < headers.Length; i++)
42+
// Get the window width if possible
43+
try
2244
{
23-
_columnWidths[i] = headers[i].Length;
45+
windowWidth = Console.WindowWidth;
46+
}
47+
catch (Exception)
48+
{
49+
// Ignore any exception
2450
}
2551

26-
_rows.Add(headers);
52+
// If the window width is not available, use the default window width
53+
if (windowWidth <= 0)
54+
{
55+
_maxColumnWidth = DefaultWindowWidth;
56+
}
57+
else
58+
{
59+
_maxColumnWidth = Math.Max(MinimumCharactersInAColumn, (windowWidth - MinimumCharactersInAColumn * headers.Length) / headers.Length);
60+
}
61+
62+
// Add the headers
63+
foreach (var header in headers)
64+
{
65+
_columns.Add(new Column { Header = header, Width = header.Length });
66+
}
2767
}
2868

69+
/* Add a row to the table
70+
* row: The list of values in the row
71+
*/
2972
public void AddRow(params string[] row)
3073
{
31-
if (row.Length != _columnWidths.Length)
74+
if (row.Length != _columns.Count)
3275
{
3376
throw new InvalidOperationException("Row column count does not match header column count.");
3477
}
3578

3679
for (int i = 0; i < row.Length; i++)
3780
{
38-
_columnWidths[i] = Math.Max(_columnWidths[i], row[i]?.Length ?? 0);
81+
_columns[i].Width = Math.Min(_maxColumnWidth, Math.Max(_columns[i].Width, row[i]?.Length ?? 0));
3982
}
4083

4184
_rows.Add(row);
4285
}
4386

44-
public void PrintResult(string searchTerm, ILoggerWithColor logger)
87+
/* Print the table with highlighting
88+
* logger: The logger to use for printing
89+
* highlightTerm: The term to highlight in the table
90+
*/
91+
public void PrintResult(string highlightTerm, ILoggerWithColor logger)
4592
{
46-
ConsoleColor consoleColor = Console.ForegroundColor;
47-
ConsoleColor highlighterColor = GetHighlighterColor();
48-
49-
// If only headers are present (i.e., no package rows)
50-
if (_rows.Count <= 1)
93+
if (_rows.Count == 0)
5194
{
5295
logger.LogMinimal("No results found.");
5396
return;
5497
}
98+
// Print the header
99+
PrintRow(logger, _columns.Select(c => c.Header).ToList(), highlightTerm);
100+
// Print a separator line
101+
PrintRow(logger, _columns.Select(c => "".PadRight(c.Width, '-')).ToList(), "");
55102

56-
foreach (var row in _rows)
103+
foreach (string[] row in _rows)
57104
{
58-
for (int i = 0; i < row.Length; i++)
105+
// Sanitize the values to remove new lines and tabs
106+
List<string> sanitizedValues = row.Select(v => SanitizeString(v)).ToList();
107+
PrintRow(logger, sanitizedValues, highlightTerm);
108+
109+
// Print a separator line
110+
PrintRow(logger, _columns.Select(c => "".PadRight(c.Width, '-')).ToList(), "");
111+
}
112+
}
113+
114+
private string SanitizeString(string value)
115+
{
116+
return Regex.Replace(value ?? string.Empty, @"\r\n|\n\r|\n|\r|\t", " ");
117+
}
118+
119+
/* Print a row in the table
120+
* logger: The logger to use for printing
121+
* values: The list of values in the row
122+
* highlightTerm: The term to highlight in the row
123+
*/
124+
private void PrintRow(ILoggerWithColor logger, List<string> values, string highlightTerm)
125+
{
126+
ConsoleColor color = _consoleColor;
127+
128+
// In one row there could be multiple rows if the value is too long. subRow is the index of the sub row
129+
int subRow = 0;
130+
// Keep track of the columns that have been printed
131+
List<int> renderedColumns = new List<int>();
132+
bool done = false;
133+
134+
List<List<int>> highlight = new List<List<int>>();
135+
136+
// Find the indices of the highlight term in each value
137+
foreach (string value in values)
138+
{
139+
highlight.Add(FindSubstringIndices(value, highlightTerm));
140+
}
141+
142+
// Keep printing the row until all the columns have been printed
143+
while (!done)
144+
{
145+
// Print column by column
146+
for (int column = 0; column < _columns.Count; column++)
59147
{
60-
var paddedValue = (row[i] ?? string.Empty).PadRight(_columnWidths[i]);
148+
logger.LogMinimal("| ", color);
149+
string value = values[column];
61150

62-
if (!string.IsNullOrEmpty(searchTerm) && paddedValue.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0)
151+
// For each column, print character by character with the appropriate color
152+
for (int i = 0; i < _columns[column].Width; i++)
63153
{
64-
logger.LogMinimal("| ", consoleColor);
65-
if (_columnsToHighlight.Contains(i))
154+
int CharacterIndex = subRow * _columns[column].Width + i;
155+
156+
// Change to highlighter color if the character index is within the highlight term
157+
if (_columnsToHighlight.Contains(column) && highlight[column].Contains(CharacterIndex))
66158
{
67-
PrintWithHighlight(paddedValue, searchTerm, highlighterColor, logger);
159+
color = _highlighter;
160+
}
161+
162+
// All the characters have been printed
163+
if (CharacterIndex >= value.Length)
164+
{
165+
if (!renderedColumns.Contains(column))
166+
{
167+
renderedColumns.Add(column);
168+
}
169+
170+
logger.LogMinimal("".PadRight(_columns[column].Width - i), color);
171+
break;
172+
}
173+
174+
// If the character index is within the length of the value, print the character
175+
if (CharacterIndex < value.Length)
176+
{
177+
logger.LogMinimal(value[CharacterIndex].ToString(), color);
68178
}
69179
else
70180
{
71-
logger.LogMinimal(paddedValue, consoleColor);
181+
logger.LogMinimal(" ", color);
72182
}
73-
logger.LogMinimal(" ", consoleColor);
74-
}
75-
else
76-
{
77-
logger.LogMinimal("| " + paddedValue + " ", consoleColor);
183+
184+
// If the character index is the last character in the value, add the column to the list of rendered columns
185+
if (CharacterIndex == value.Length - 1)
186+
{
187+
if (!renderedColumns.Contains(column))
188+
{
189+
renderedColumns.Add(column);
190+
}
191+
}
192+
193+
// Reset the color to the default color
194+
color = _consoleColor;
78195
}
196+
197+
logger.LogMinimal(" ", color);
79198
}
80199

81-
logger.LogMinimal("|");
200+
// New line for new row
201+
logger.LogMinimal("|", color);
202+
logger.LogMinimal("");
203+
subRow++;
82204

83-
if (row == _rows.First())
205+
// If all the columns have been printed, we are done
206+
if (renderedColumns.Count >= values.Count)
84207
{
85-
// Add the separator after the header.
86-
foreach (var width in _columnWidths)
87-
{
88-
logger.LogMinimal("|" + new string('-', width + 2), consoleColor);
89-
}
90-
91-
logger.LogMinimal("|");
208+
done = true;
92209
}
93210
}
94211
}
95212

96-
private static void PrintWithHighlight(string value, string searchTerm, ConsoleColor highlighterColor, ILoggerWithColor logger)
213+
private static List<int> FindSubstringIndices(string str, string substring)
97214
{
98-
int index = value.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase);
99-
ConsoleColor originalColor = Console.ForegroundColor;
215+
List<int> indices = new List<int>();
100216

101-
while (index != -1)
217+
if (string.IsNullOrEmpty(substring))
102218
{
103-
logger.LogMinimal(value.Substring(0, index), originalColor);
104-
logger.LogMinimal(value.Substring(index, searchTerm.Length), highlighterColor);
105-
value = value.Substring(index + searchTerm.Length);
106-
index = value.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase);
219+
return indices;
107220
}
108221

109-
logger.LogMinimal(value, originalColor);
110-
}
111-
112-
private static ConsoleColor GetHighlighterColor()
113-
{
114-
if (Console.ForegroundColor == ConsoleColor.Red || Console.BackgroundColor == ConsoleColor.Red)
222+
int index = 0;
223+
while ((index = str.IndexOf(substring, index, StringComparison.CurrentCultureIgnoreCase)) != -1)
115224
{
116-
return ConsoleColor.Blue;
225+
for (int i = 0; i < substring.Length; i++)
226+
{
227+
indices.Add(index + i);
228+
}
229+
index += substring.Length;
117230
}
118231

119-
return ConsoleColor.Red;
232+
return indices;
120233
}
121234
}
122235
}

0 commit comments

Comments
 (0)