Add profile support

Closes #231
This commit is contained in:
Patrik Svensson
2021-01-16 17:23:58 +01:00
committed by Patrik Svensson
parent 913a7b1e37
commit a23bec4082
230 changed files with 1241 additions and 1628 deletions

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -0,0 +1,41 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents how an exception is formatted.
/// </summary>
[Flags]
public enum ExceptionFormats
{
/// <summary>
/// The default formatting.
/// </summary>
Default = 0,
/// <summary>
/// Whether or not paths should be shortened.
/// </summary>
ShortenPaths = 1,
/// <summary>
/// Whether or not types should be shortened.
/// </summary>
ShortenTypes = 2,
/// <summary>
/// Whether or not methods should be shortened.
/// </summary>
ShortenMethods = 4,
/// <summary>
/// Whether or not to show paths as links in the terminal.
/// </summary>
ShowLinks = 8,
/// <summary>
/// Shortens everything that can be shortened.
/// </summary>
ShortenEverything = ShortenMethods | ShortenTypes | ShortenPaths,
}
}

View File

@ -0,0 +1,171 @@
using System;
using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal static class ExceptionFormatter
{
public static IRenderable Format(Exception exception, ExceptionSettings settings)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var info = ExceptionParser.Parse(exception.ToString());
if (info == null)
{
return new Text(exception.ToString());
}
return GetException(info, settings);
}
private static IRenderable GetException(ExceptionInfo info, ExceptionSettings settings)
{
if (info is null)
{
throw new ArgumentNullException(nameof(info));
}
return new Rows(new IRenderable[]
{
GetMessage(info, settings),
GetStackFrames(info, settings),
}).Expand();
}
private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings)
{
var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
}
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings)
{
var styles = settings.Style;
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
// Inner
if (ex.Inner != null)
{
grid.AddRow(
Text.Empty,
GetException(ex.Inner, settings));
}
// Stack frames
foreach (var frame in ex.Frames)
{
var builder = new StringBuilder();
// Method
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, styles.Method, shortenMethods, settings));
builder.AppendWithStyle(styles.Parenthesis, "(");
AppendParameters(builder, frame, settings);
builder.AppendWithStyle(styles.Parenthesis, ")");
if (frame.Path != null)
{
builder.Append(' ');
builder.AppendWithStyle(styles.Dimmed, "in");
builder.Append(' ');
// Path
AppendPath(builder, frame, settings);
// Line number
if (frame.LineNumber != null)
{
builder.AppendWithStyle(styles.Dimmed, ":");
builder.AppendWithStyle(styles.LineNumber, frame.LineNumber);
}
}
grid.AddRow(
$"[{styles.Dimmed.ToMarkup()}]at[/]",
builder.ToString());
}
return grid;
}
private static void AppendParameters(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
var typeColor = settings.Style.ParameterType.ToMarkup();
var nameColor = settings.Style.ParameterName.ToMarkup();
var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.EscapeMarkup()}[/] [{nameColor}]{x.Name.EscapeMarkup()}[/]");
builder.Append(string.Join(", ", parameters));
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
if (frame?.Path is null)
{
return;
}
void AppendPath()
{
var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings));
}
if ((settings.Format & ExceptionFormats.ShowLinks) != 0)
{
var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null)
{
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
}
AppendPath();
if (hasLink && uri != null)
{
builder.Append("[/]");
}
}
else
{
AppendPath();
}
}
private static string Emphasize(string input, char[] separators, Style color, bool compact, ExceptionSettings settings)
{
var builder = new StringBuilder();
var type = input;
var index = type.LastIndexOfAny(separators);
if (index != -1)
{
if (!compact)
{
builder.AppendWithStyle(
settings.Style.NonEmphasized,
type.Substring(0, index + 1).EscapeMarkup());
}
builder.AppendWithStyle(
color,
type.Substring(index + 1, type.Length - index - 1).EscapeMarkup());
}
else
{
builder.Append(type.EscapeMarkup());
}
return builder.ToString();
}
}
}

View File

@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Spectre.Console
{
internal sealed class ExceptionInfo
{
public string Type { get; }
public string Message { get; }
public List<StackFrameInfo> Frames { get; }
public ExceptionInfo? Inner { get; }
public ExceptionInfo(
string type, string message,
List<StackFrameInfo> frames,
ExceptionInfo? inner)
{
Type = type ?? string.Empty;
Message = message ?? string.Empty;
Frames = frames ?? new List<StackFrameInfo>();
Inner = inner;
}
}
}

View File

@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Spectre.Console
{
internal static class ExceptionParser
{
private static readonly Regex _messageRegex = new Regex(@"^(?'type'.*):\s(?'message'.*)$");
private static readonly Regex _stackFrameRegex = new Regex(@"^\s*\w*\s(?'method'.*)\((?'params'.*)\)");
private static readonly Regex _fullStackFrameRegex = new Regex(@"^\s*(?'at'\w*)\s(?'method'.*)\((?'params'.*)\)\s(?'in'\w*)\s(?'path'.*)\:(?'line'\w*)\s(?'linenumber'\d*)$");
public static ExceptionInfo? Parse(string exception)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var lines = exception.SplitLines();
return Parse(new Queue<string>(lines));
}
private static ExceptionInfo? Parse(Queue<string> lines)
{
if (lines.Count == 0)
{
// Error: No lines to parse
return null;
}
var line = lines.Dequeue();
line = line.ReplaceExact(" ---> ", string.Empty);
var match = _messageRegex.Match(line);
if (!match.Success)
{
return null;
}
var inner = (ExceptionInfo?)null;
// Stack frames
var frames = new List<StackFrameInfo>();
while (lines.Count > 0)
{
if (lines.Peek().TrimStart().StartsWith("---> ", StringComparison.OrdinalIgnoreCase))
{
inner = Parse(lines);
if (inner == null)
{
// Error: Could not parse inner exception
return null;
}
continue;
}
line = lines.Dequeue();
if (string.IsNullOrWhiteSpace(line))
{
// Empty line
continue;
}
if (line.TrimStart().StartsWith("--- ", StringComparison.OrdinalIgnoreCase))
{
// End of inner exception
break;
}
var stackFrame = ParseStackFrame(line);
if (stackFrame == null)
{
// Error: Could not parse stack frame
return null;
}
frames.Add(stackFrame);
}
return new ExceptionInfo(
match.Groups["type"].Value,
match.Groups["message"].Value,
frames, inner);
}
private static StackFrameInfo? ParseStackFrame(string frame)
{
var match = _fullStackFrameRegex.Match(frame);
if (match?.Success != true)
{
match = _stackFrameRegex.Match(frame);
if (match?.Success != true)
{
return null;
}
}
var parameters = ParseMethodParameters(match.Groups["params"].Value);
if (parameters == null)
{
// Error: Could not parse parameters
return null;
}
var method = match.Groups["method"].Value;
var path = match.Groups["path"].Success ? match.Groups["path"].Value : null;
var lineNumber = (int?)null;
if (!string.IsNullOrWhiteSpace(match.Groups["linenumber"].Value))
{
lineNumber = int.Parse(match.Groups["linenumber"].Value, CultureInfo.InvariantCulture);
}
return new StackFrameInfo(method, parameters, path, lineNumber);
}
private static List<(string Type, string Name)>? ParseMethodParameters(string parameters)
{
var result = new List<(string Type, string Name)>();
foreach (var parameterPart in parameters.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries))
{
var parameterNameIndex = parameterPart.LastIndexOf(' ');
if (parameterNameIndex == -1)
{
// Error: Could not parse parameter
return null;
}
var type = parameterPart.Substring(0, parameterNameIndex);
var name = parameterPart.Substring(parameterNameIndex + 1, parameterPart.Length - parameterNameIndex - 1);
result.Add((type, name));
}
return result;
}
}
}

View File

@ -0,0 +1,27 @@
namespace Spectre.Console
{
/// <summary>
/// Exception settings.
/// </summary>
public sealed class ExceptionSettings
{
/// <summary>
/// Gets or sets the exception format.
/// </summary>
public ExceptionFormats Format { get; set; }
/// <summary>
/// Gets or sets the exception style.
/// </summary>
public ExceptionStyle Style { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionSettings"/> class.
/// </summary>
public ExceptionSettings()
{
Format = ExceptionFormats.Default;
Style = new ExceptionStyle();
}
}
}

View File

@ -0,0 +1,58 @@
namespace Spectre.Console
{
/// <summary>
/// Represent an exception style.
/// </summary>
public sealed class ExceptionStyle
{
/// <summary>
/// Gets or sets the message color.
/// </summary>
public Style Message { get; set; } = new Style(Color.Red, Color.Default, Decoration.Bold);
/// <summary>
/// Gets or sets the exception color.
/// </summary>
public Style Exception { get; set; } = new Style(Color.White);
/// <summary>
/// Gets or sets the method color.
/// </summary>
public Style Method { get; set; } = new Style(Color.Yellow);
/// <summary>
/// Gets or sets the parameter type color.
/// </summary>
public Style ParameterType { get; set; } = new Style(Color.Blue);
/// <summary>
/// Gets or sets the parameter name color.
/// </summary>
public Style ParameterName { get; set; } = new Style(Color.Silver);
/// <summary>
/// Gets or sets the parenthesis color.
/// </summary>
public Style Parenthesis { get; set; } = new Style(Color.Silver);
/// <summary>
/// Gets or sets the path color.
/// </summary>
public Style Path { get; set; } = new Style(Color.Yellow, Color.Default, Decoration.Bold);
/// <summary>
/// Gets or sets the line number color.
/// </summary>
public Style LineNumber { get; set; } = new Style(Color.Blue);
/// <summary>
/// Gets or sets the color for dimmed text such as "at" or "in".
/// </summary>
public Style Dimmed { get; set; } = new Style(Color.Grey);
/// <summary>
/// Gets or sets the color for non emphasized items.
/// </summary>
public Style NonEmphasized { get; set; } = new Style(Color.Silver);
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
namespace Spectre.Console
{
internal sealed class StackFrameInfo
{
public string Method { get; }
public List<(string Type, string Name)> Parameters { get; }
public string? Path { get; }
public int? LineNumber { get; }
public StackFrameInfo(
string method, List<(string Type, string Name)> parameters,
string? path, int? lineNumber)
{
Method = method ?? throw new ArgumentNullException(nameof(method));
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
Path = path;
LineNumber = lineNumber;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? result)
{
try
{
if (Path == null)
{
result = null;
return false;
}
if (!Uri.TryCreate(Path, UriKind.Absolute, out var uri))
{
result = null;
return false;
}
if (uri.Scheme == "file")
{
// For local files, we need to append
// the host name. Otherwise the terminal
// will most probably not allow it.
var builder = new UriBuilder(uri)
{
Host = Dns.GetHostName(),
};
uri = builder.Uri;
}
result = uri;
return true;
}
catch
{
result = null;
return false;
}
}
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using Spectre.Console.Internal;
namespace Spectre.Console
{

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,6 +1,5 @@
using System;
using System.Globalization;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,6 +1,5 @@
using System;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,6 +1,5 @@
using System;
using System.Globalization;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
@ -148,8 +147,8 @@ namespace Spectre.Console
private ProgressRenderer CreateRenderer()
{
var caps = _console.Capabilities;
var interactive = caps.SupportsInteraction && caps.SupportsAnsi;
var caps = _console.Profile.Capabilities;
var interactive = caps.Interactive && caps.Ansi;
if (interactive)
{

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Spectre.Console.Internal;
namespace Spectre.Console
{
@ -22,8 +20,6 @@ namespace Spectre.Console
/// </summary>
public bool IsFinished => _tasks.All(task => task.IsFinished);
internal Encoding Encoding => _console.Encoding;
internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
{
_tasks = new List<ProgressTask>();

View File

@ -1,7 +1,7 @@
using System;
using System.Threading;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class ProgressRefreshThread : IDisposable
{

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal abstract class ProgressRenderer : IRenderHook
{

View File

@ -1,6 +1,6 @@
using System;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal readonly struct ProgressSample
{

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
namespace Spectre.Console
{

View File

@ -4,7 +4,7 @@ using System.Diagnostics;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class DefaultProgressRenderer : ProgressRenderer
{
@ -60,7 +60,7 @@ namespace Spectre.Console.Internal
_stopwatch.Start();
}
var renderContext = new RenderContext(_console.Encoding, _console.Capabilities.LegacyConsole);
var renderContext = new RenderContext(_console.Profile.Encoding, _console.Profile.Capabilities.Legacy);
var delta = _stopwatch.Elapsed - _lastUpdate;
_lastUpdate = _stopwatch.Elapsed;

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class FallbackProgressRenderer : ProgressRenderer
{

View File

@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class StatusFallbackRenderer : ProgressRenderer
internal sealed class FallbackStatusRenderer : ProgressRenderer
{
private readonly object _lock;
private IRenderable? _renderable;
@ -13,7 +13,7 @@ namespace Spectre.Console.Internal
public override TimeSpan RefreshRate => TimeSpan.FromMilliseconds(100);
public StatusFallbackRenderer()
public FallbackStatusRenderer()
{
_lock = new object();
}

View File

@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using Spectre.Console.Internal;
namespace Spectre.Console
{
@ -107,7 +106,7 @@ namespace Spectre.Console
var progress = new Progress(_console)
{
FallbackRenderer = new StatusFallbackRenderer(),
FallbackRenderer = new FallbackStatusRenderer(),
AutoClear = true,
AutoRefresh = AutoRefresh,
};

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
@ -63,14 +62,14 @@ namespace Spectre.Console
/// <inheritdoc/>
public List<T> Show(IAnsiConsole console)
{
if (!console.Capabilities.SupportsInteraction)
if (!console.Profile.Capabilities.Interactive)
{
throw new NotSupportedException(
"Cannot show multi selection prompt since the current " +
"terminal isn't interactive.");
}
if (!console.Capabilities.SupportsAnsi)
if (!console.Profile.Capabilities.Ansi)
{
throw new NotSupportedException(
"Cannot show multi selection prompt since the current " +

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -45,9 +45,9 @@ namespace Spectre.Console
protected override int CalculatePageSize(int requestedPageSize)
{
var pageSize = requestedPageSize;
if (pageSize > _console.Height - 5)
if (pageSize > _console.Profile.Height - 5)
{
pageSize = _console.Height - 5;
pageSize = _console.Profile.Height - 5;
}
return pageSize;

View File

@ -23,9 +23,9 @@ namespace Spectre.Console
protected override int CalculatePageSize(int requestedPageSize)
{
var pageSize = requestedPageSize;
if (pageSize > _console.Height - 4)
if (pageSize > _console.Profile.Height - 4)
{
pageSize = _console.Height - 4;
pageSize = _console.Profile.Height - 4;
}
return pageSize;

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
@ -50,14 +49,14 @@ namespace Spectre.Console
/// <inheritdoc/>
T IPrompt<T>.Show(IAnsiConsole console)
{
if (!console.Capabilities.SupportsInteraction)
if (!console.Profile.Capabilities.Interactive)
{
throw new NotSupportedException(
"Cannot show selection prompt since the current " +
"terminal isn't interactive.");
}
if (!console.Capabilities.SupportsAnsi)
if (!console.Profile.Capabilities.Ansi)
{
throw new NotSupportedException(
"Cannot show selection prompt since the current " +

View File

@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using Spectre.Console.Internal;
namespace Spectre.Console
{

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console

View File

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console