Add various exception improvements

This commit is contained in:
Patrik Svensson 2020-10-06 18:23:16 +02:00 committed by Patrik Svensson
parent 39a8588dc3
commit 68e92f3365
16 changed files with 417 additions and 42 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@ -9,9 +9,9 @@ You can make exception a bit more readable by using the `WriteException` method.
AnsiConsole.WriteException(ex);
```
<img src="assets/images/exception.png" style="max-width: 100%; margin-bottom: 20px">
<img src="assets/images/exception.png" style="max-width: 100%;">
## Shortening parts
You can also shorten specific parts of the exception to make it even
more readable, and make paths clickable hyperlinks. Whether or not
@ -24,3 +24,29 @@ AnsiConsole.WriteException(ex,
```
<img src="assets/images/compact_exception.png" style="max-width: 100%;">
## Customizing exception output
In addition to shorten specific part of the exception, you can
also override the default styling.
```csharp
AnsiConsole.WriteException(ex, new ExceptionSettings
{
Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks,
Style = new ExceptionStyle
{
Exception = Style.WithForeground(Color.Grey),
Message = Style.WithForeground(Color.White),
NonEmphasized = Style.WithForeground(Color.Cornsilk1),
Parenthesis = Style.WithForeground(Color.Cornsilk1),
Method = Style.WithForeground(Color.Red),
ParameterName = Style.WithForeground(Color.Cornsilk1),
ParameterType = Style.WithForeground(Color.Red),
Path = Style.WithForeground(Color.Red),
LineNumber = Style.WithForeground(Color.Cornsilk1),
}
});
```
<img src="assets/images/custom_exception.png" style="max-width: 100%;">

View File

@ -43,6 +43,12 @@ AnsiConsole.Markup("[[Hello]] "); // [Hello]
AnsiConsole.Markup("[red][[World]][/]"); // [World]
```
You can also use the `SafeMarkup` extension method.
```csharp
AnsiConsole.Markup("[red]{0}[/]", "Hello [World]".SafeMarkup());
```
# Setting background color
You can set the background color in markup by prefixing the color with

View File

@ -14,11 +14,35 @@ namespace Exceptions
}
catch (Exception ex)
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel("[u]Default[/]").Expand());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex);
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel("[u]Compact[/]").Expand());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks);
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel("[u]Custom colors[/]").Expand());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex, new ExceptionSettings
{
Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks,
Style = new ExceptionStyle
{
Exception = Style.WithForeground(Color.Grey),
Message = Style.WithForeground(Color.White),
NonEmphasized = Style.WithForeground(Color.Cornsilk1),
Parenthesis = Style.WithForeground(Color.Cornsilk1),
Method = Style.WithForeground(Color.Red),
ParameterName = Style.WithForeground(Color.Cornsilk1),
ParameterType = Style.WithForeground(Color.Red),
Path = Style.WithForeground(Color.Red),
LineNumber = Style.WithForeground(Color.Cornsilk1),
}
});
}
}

View File

@ -228,6 +228,39 @@ namespace Spectre.Console.Tests.Unit
}
}
public sealed class TheToMarkupMethod
{
[Fact]
public void Should_Return_Expected_Markup_For_Default_Color()
{
// Given, When
var result = Color.Default.ToMarkup();
// Then
result.ShouldBe("default");
}
[Fact]
public void Should_Return_Expected_Markup_For_Known_Color()
{
// Given, When
var result = Color.Red.ToMarkup();
// Then
result.ShouldBe("red");
}
[Fact]
public void Should_Return_Expected_Markup_For_Custom_Color()
{
// Given, When
var result = new Color(255, 1, 12).ToMarkup();
// Then
result.ShouldBe("#FF010C");
}
}
public sealed class TheToStringMethod
{
[Fact]

View File

@ -317,5 +317,60 @@ namespace Spectre.Console.Tests.Unit
result.ShouldBeFalse();
}
}
public sealed class TheToMarkupMethod
{
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Foreground_Color()
{
// Given
var style = new Style(Color.Red);
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("red");
}
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Foreground_And_Background_Color()
{
// Given
var style = new Style(Color.Red, Color.Green);
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("red on green");
}
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Foreground_And_Background_Color_And_Decoration()
{
// Given
var style = new Style(Color.Red, Color.Green, Decoration.Bold | Decoration.Underline);
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("bold underline red on green");
}
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Only_Background_Color()
{
// Given
var style = new Style(background: Color.Green);
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("default on green");
}
}
}
}

View File

@ -16,5 +16,15 @@ namespace Spectre.Console
{
Console.WriteException(exception, format);
}
/// <summary>
/// Writes an exception to the console.
/// </summary>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="settings">The exception settings.</param>
public static void WriteException(Exception exception, ExceptionSettings settings)
{
Console.WriteException(exception, settings);
}
}
}

View File

@ -249,8 +249,13 @@ namespace Spectre.Console
/// Converts the color to a markup string.
/// </summary>
/// <returns>A <see cref="string"/> representing the color as markup.</returns>
public string ToMarkupString()
public string ToMarkup()
{
if (IsDefault)
{
return "default";
}
if (Number != null)
{
var name = ColorTable.GetName(Number.Value);

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

@ -17,5 +17,16 @@ namespace Spectre.Console
{
Render(console, exception.GetRenderable(format));
}
/// <summary>
/// Writes an exception to the console.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="settings">The exception settings.</param>
public static void WriteException(this IAnsiConsole console, Exception exception, ExceptionSettings settings)
{
Render(console, exception.GetRenderable(settings));
}
}
}

View File

@ -16,7 +16,36 @@ namespace Spectre.Console
/// <returns>A <see cref="IRenderable"/> representing the exception.</returns>
public static IRenderable GetRenderable(this Exception exception, ExceptionFormats format = ExceptionFormats.Default)
{
return ExceptionFormatter.Format(exception, format);
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
return GetRenderable(exception, new ExceptionSettings
{
Format = format,
});
}
/// <summary>
/// Gets a <see cref="IRenderable"/> representation of the exception.
/// </summary>
/// <param name="exception">The exception to format.</param>
/// <param name="settings">The exception settings.</param>
/// <returns>A <see cref="IRenderable"/> representing the exception.</returns>
public static IRenderable GetRenderable(this Exception exception, ExceptionSettings settings)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
return ExceptionFormatter.Format(exception, settings);
}
}
}

View File

@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Spectre.Console.Internal
{
internal static class DecorationTable
{
private static readonly Dictionary<string, Decoration?> _lookup;
private static readonly Dictionary<Decoration, string> _reverseLookup;
[SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline")]
static DecorationTable()
@ -28,6 +30,21 @@ namespace Spectre.Console.Internal
{ "strikethrough", Decoration.Strikethrough },
{ "s", Decoration.Strikethrough },
};
_reverseLookup = new Dictionary<Decoration, string>();
foreach (var (name, decoration) in _lookup)
{
// Cannot happen, but the compiler thinks so...
if (decoration == null)
{
continue;
}
if (!_reverseLookup.ContainsKey(decoration.Value))
{
_reverseLookup[decoration.Value] = name;
}
}
}
public static Decoration? GetDecoration(string name)
@ -35,5 +52,23 @@ namespace Spectre.Console.Internal
_lookup.TryGetValue(name, out var result);
return result;
}
public static List<string> GetMarkupNames(Decoration decoration)
{
var result = new List<string>();
Enum.GetValues(typeof(Decoration))
.Cast<Decoration>()
.Where(flag => (decoration & flag) != 0)
.ForEach(flag =>
{
if (flag != Decoration.None && _reverseLookup.TryGetValue(flag, out var name))
{
result.Add(name);
}
});
return result;
}
}
}

View File

@ -8,13 +8,7 @@ namespace Spectre.Console
{
internal static class ExceptionFormatter
{
private static readonly Color _typeColor = Color.White;
private static readonly Color _methodColor = Color.Yellow;
private static readonly Color _parameterColor = Color.Blue;
private static readonly Color _pathColor = Color.Yellow;
private static readonly Color _dimmedColor = Color.Grey;
public static IRenderable Format(Exception exception, ExceptionFormats format)
public static IRenderable Format(Exception exception, ExceptionSettings settings)
{
if (exception is null)
{
@ -27,10 +21,10 @@ namespace Spectre.Console
return new Text(exception.ToString());
}
return GetException(info, format);
return GetException(info, settings);
}
private static IRenderable GetException(ExceptionInfo info, ExceptionFormats format)
private static IRenderable GetException(ExceptionInfo info, ExceptionSettings settings)
{
if (info is null)
{
@ -39,20 +33,20 @@ namespace Spectre.Console
return new Rows(new IRenderable[]
{
GetMessage(info, format),
GetStackFrames(info, format),
GetMessage(info, settings),
GetStackFrames(info, settings),
}).Expand();
}
private static Markup GetMessage(ExceptionInfo ex, ExceptionFormats format)
private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings)
{
var shortenTypes = (format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, _typeColor.ToMarkupString(), shortenTypes);
var message = $"[b red]{ex.Message.SafeMarkup()}[/]";
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.SafeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
}
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionFormats format)
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings)
{
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
@ -63,7 +57,7 @@ namespace Spectre.Console
{
grid.AddRow(
Text.Empty,
GetException(ex.Inner, format));
GetException(ex.Inner, settings));
}
// Stack frames
@ -72,47 +66,57 @@ namespace Spectre.Console
var builder = new StringBuilder();
// Method
var shortenMethods = (format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, _methodColor.ToMarkupString(), shortenMethods));
builder.Append('(');
builder.Append(string.Join(", ", frame.Parameters.Select(x => $"[{_parameterColor.ToMarkupString()}]{x.Type.SafeMarkup()}[/] {x.Name}")));
builder.Append(')');
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, settings.Style.Method, shortenMethods, settings));
builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append('(').Append("[/]");
AppendParameters(builder, frame, settings);
builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append(')').Append("[/]");
if (frame.Path != null)
{
builder.Append(" [").Append(_dimmedColor.ToMarkupString()).Append("]in[/] ");
builder.Append(" [").Append(settings.Style.Dimmed.ToMarkup()).Append("]in[/] ");
// Path
AppendPath(builder, frame, format);
AppendPath(builder, frame, settings);
// Line number
if (frame.LineNumber != null)
{
builder.Append(':');
builder.Append('[').Append(_parameterColor.ToMarkupString()).Append(']').Append(frame.LineNumber).Append("[/]");
builder.Append('[').Append(settings.Style.Dimmed.ToMarkup()).Append("]:[/]");
builder.Append('[').Append(settings.Style.LineNumber.ToMarkup()).Append(']').Append(frame.LineNumber).Append("[/]");
}
}
grid.AddRow($"[{_dimmedColor.ToMarkupString()}]at[/]", builder.ToString());
grid.AddRow(
$"[{settings.Style.Dimmed.ToMarkup()}]at[/]",
builder.ToString());
}
return grid;
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionFormats format)
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.SafeMarkup()}[/] [{nameColor}]{x.Name}[/]");
builder.Append(string.Join(", ", parameters));
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
if (frame?.Path is null)
{
return;
}
void RenderLink()
void AppendPath()
{
var shortenPaths = (format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, $"b {_pathColor.ToMarkupString()}", shortenPaths));
var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings));
}
if ((format & ExceptionFormats.ShowLinks) != 0)
if ((settings.Format & ExceptionFormats.ShowLinks) != 0)
{
var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null)
@ -120,7 +124,7 @@ namespace Spectre.Console
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
}
RenderLink();
AppendPath();
if (hasLink && uri != null)
{
@ -129,11 +133,11 @@ namespace Spectre.Console
}
else
{
RenderLink();
AppendPath();
}
}
private static string Emphasize(string input, char[] separators, string color, bool compact)
private static string Emphasize(string input, char[] separators, Style color, bool compact, ExceptionSettings settings)
{
var builder = new StringBuilder();
@ -143,10 +147,12 @@ namespace Spectre.Console
{
if (!compact)
{
builder.Append("[silver]").Append(type, 0, index + 1).Append("[/]");
builder.Append('[').Append(settings.Style.NonEmphasized.ToMarkup()).Append(']')
.Append(type, 0, index + 1).Append("[/]");
}
builder.Append('[').Append(color).Append(']').Append(type, index + 1, type.Length - index - 1).Append("[/]");
builder.Append('[').Append(color.ToMarkup()).Append(']')
.Append(type, index + 1, type.Length - index - 1).Append("[/]");
}
else
{

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Spectre.Console.Internal
{
internal static class DictionaryExtensions
{
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
{
key = tuple.Key;
value = tuple.Value;
}
}
}

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Spectre.Console.Internal;
namespace Spectre.Console
@ -25,7 +27,7 @@ namespace Spectre.Console
public Decoration Decoration { get; }
/// <summary>
/// Gets the link.
/// Gets the link associated with the style.
/// </summary>
public string? Link { get; }
@ -191,6 +193,41 @@ namespace Spectre.Console
}
}
/// <summary>
/// Returns the markup representation of this style.
/// </summary>
/// <returns>The markup representation of this style.</returns>
public string ToMarkup()
{
var builder = new List<string>();
if (Decoration != Decoration.None)
{
var result = DecorationTable.GetMarkupNames(Decoration);
if (result.Count != 0)
{
builder.AddRange(result);
}
}
if (Foreground != Color.Default)
{
builder.Add(Foreground.ToMarkup());
}
if (Background != Color.Default)
{
if (builder.Count == 0)
{
builder.Add("default");
}
builder.Add("on " + Background.ToMarkup());
}
return string.Join(" ", builder);
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{