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); 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 You can also shorten specific parts of the exception to make it even
more readable, and make paths clickable hyperlinks. Whether or not 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%;"> <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] AnsiConsole.Markup("[red][[World]][/]"); // [World]
``` ```
You can also use the `SafeMarkup` extension method.
```csharp
AnsiConsole.Markup("[red]{0}[/]", "Hello [World]".SafeMarkup());
```
# Setting background color # Setting background color
You can set the background color in markup by prefixing the color with You can set the background color in markup by prefixing the color with

View File

@ -14,11 +14,35 @@ namespace Exceptions
} }
catch (Exception ex) catch (Exception ex)
{ {
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel("[u]Default[/]").Expand());
AnsiConsole.WriteLine(); AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex); AnsiConsole.WriteException(ex);
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel("[u]Compact[/]").Expand());
AnsiConsole.WriteLine(); AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks); 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 public sealed class TheToStringMethod
{ {
[Fact] [Fact]

View File

@ -317,5 +317,60 @@ namespace Spectre.Console.Tests.Unit
result.ShouldBeFalse(); 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); 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. /// Converts the color to a markup string.
/// </summary> /// </summary>
/// <returns>A <see cref="string"/> representing the color as markup.</returns> /// <returns>A <see cref="string"/> representing the color as markup.</returns>
public string ToMarkupString() public string ToMarkup()
{ {
if (IsDefault)
{
return "default";
}
if (Number != null) if (Number != null)
{ {
var name = ColorTable.GetName(Number.Value); 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)); 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> /// <returns>A <see cref="IRenderable"/> representing the exception.</returns>
public static IRenderable GetRenderable(this Exception exception, ExceptionFormats format = ExceptionFormats.Default) 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Spectre.Console.Internal namespace Spectre.Console.Internal
{ {
internal static class DecorationTable internal static class DecorationTable
{ {
private static readonly Dictionary<string, Decoration?> _lookup; private static readonly Dictionary<string, Decoration?> _lookup;
private static readonly Dictionary<Decoration, string> _reverseLookup;
[SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline")] [SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline")]
static DecorationTable() static DecorationTable()
@ -28,6 +30,21 @@ namespace Spectre.Console.Internal
{ "strikethrough", Decoration.Strikethrough }, { "strikethrough", Decoration.Strikethrough },
{ "s", 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) public static Decoration? GetDecoration(string name)
@ -35,5 +52,23 @@ namespace Spectre.Console.Internal
_lookup.TryGetValue(name, out var result); _lookup.TryGetValue(name, out var result);
return 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 internal static class ExceptionFormatter
{ {
private static readonly Color _typeColor = Color.White; public static IRenderable Format(Exception exception, ExceptionSettings settings)
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)
{ {
if (exception is null) if (exception is null)
{ {
@ -27,10 +21,10 @@ namespace Spectre.Console
return new Text(exception.ToString()); 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) if (info is null)
{ {
@ -39,20 +33,20 @@ namespace Spectre.Console
return new Rows(new IRenderable[] return new Rows(new IRenderable[]
{ {
GetMessage(info, format), GetMessage(info, settings),
GetStackFrames(info, format), GetStackFrames(info, settings),
}).Expand(); }).Expand();
} }
private static Markup GetMessage(ExceptionInfo ex, ExceptionFormats format) private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings)
{ {
var shortenTypes = (format & ExceptionFormats.ShortenTypes) != 0; var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, _typeColor.ToMarkupString(), shortenTypes); var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var message = $"[b red]{ex.Message.SafeMarkup()}[/]"; var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.SafeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message)); 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(); var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap()); grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
@ -63,7 +57,7 @@ namespace Spectre.Console
{ {
grid.AddRow( grid.AddRow(
Text.Empty, Text.Empty,
GetException(ex.Inner, format)); GetException(ex.Inner, settings));
} }
// Stack frames // Stack frames
@ -72,47 +66,57 @@ namespace Spectre.Console
var builder = new StringBuilder(); var builder = new StringBuilder();
// Method // Method
var shortenMethods = (format & ExceptionFormats.ShortenMethods) != 0; var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, _methodColor.ToMarkupString(), shortenMethods)); builder.Append(Emphasize(frame.Method, new[] { '.' }, settings.Style.Method, shortenMethods, settings));
builder.Append('('); builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append('(').Append("[/]");
builder.Append(string.Join(", ", frame.Parameters.Select(x => $"[{_parameterColor.ToMarkupString()}]{x.Type.SafeMarkup()}[/] {x.Name}"))); AppendParameters(builder, frame, settings);
builder.Append(')'); builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append(')').Append("[/]");
if (frame.Path != null) if (frame.Path != null)
{ {
builder.Append(" [").Append(_dimmedColor.ToMarkupString()).Append("]in[/] "); builder.Append(" [").Append(settings.Style.Dimmed.ToMarkup()).Append("]in[/] ");
// Path // Path
AppendPath(builder, frame, format); AppendPath(builder, frame, settings);
// Line number // Line number
if (frame.LineNumber != null) if (frame.LineNumber != null)
{ {
builder.Append(':'); builder.Append('[').Append(settings.Style.Dimmed.ToMarkup()).Append("]:[/]");
builder.Append('[').Append(_parameterColor.ToMarkupString()).Append(']').Append(frame.LineNumber).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; 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) if (frame?.Path is null)
{ {
return; return;
} }
void RenderLink() void AppendPath()
{ {
var shortenPaths = (format & ExceptionFormats.ShortenPaths) != 0; var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, $"b {_pathColor.ToMarkupString()}", shortenPaths)); 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); var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null) if (hasLink && uri != null)
@ -120,7 +124,7 @@ namespace Spectre.Console
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']'); builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
} }
RenderLink(); AppendPath();
if (hasLink && uri != null) if (hasLink && uri != null)
{ {
@ -129,11 +133,11 @@ namespace Spectre.Console
} }
else 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(); var builder = new StringBuilder();
@ -143,10 +147,12 @@ namespace Spectre.Console
{ {
if (!compact) 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 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;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Spectre.Console.Internal; using Spectre.Console.Internal;
namespace Spectre.Console namespace Spectre.Console
@ -25,7 +27,7 @@ namespace Spectre.Console
public Decoration Decoration { get; } public Decoration Decoration { get; }
/// <summary> /// <summary>
/// Gets the link. /// Gets the link associated with the style.
/// </summary> /// </summary>
public string? Link { get; } 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/> /// <inheritdoc/>
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {