Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions Examples/CaretDiagnostics/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,7 @@ public static void Main(string[] args)
new HighlightedSourceRenderer(5)
});

log = new TransformLog(
log,
new Func<LogEntry, LogEntry>[]
{
MakeDiagnostic
});
log = log.WithDiagnostics("program");

var doc = new StringDocument("code.cs", SourceCode);
var ctorStartOffset = SourceCode.IndexOf("public Program()");
Expand All @@ -62,13 +57,6 @@ public static void Main(string[] args)
new HighlightedSource(highlightRegion, focusRegion)
}));
}


private static LogEntry MakeDiagnostic(LogEntry entry)
{
return DiagnosticExtractor.Transform(entry, new Text("program"));
}

private const string SourceCode = @"public static class Program
{
public Program()
Expand Down
46 changes: 28 additions & 18 deletions Pixie/Markup/Diagnostic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,38 @@
namespace Pixie.Markup
{
/// <summary>
/// Describes a diagnostic as issued by typical command-line tools:
/// a self-contained nugget of information to the user.
/// Describes a compiler-style diagnostic such as an error, warning, or
/// informational message.
/// A diagnostic combines an origin, a kind, an optional title, and a
/// message body into a single markup node that can render as a header like
/// <c>file.cs:12:4: error: expected expression</c> followed by additional
/// context such as highlighted source code.
/// </summary>
public sealed class Diagnostic : MarkupNode
{
/// <summary>
/// Creates a diagnostic.
/// Creates a diagnostic with a header and message body.
/// </summary>
/// <param name="origin">
/// The origin of the diagnostic: the line of code
/// or application that causes the diagnostic to be
/// issued.
/// The origin of the diagnostic, such as a source location or
/// application name.
/// </param>
/// <param name="kind">
/// A single-word description of the kind of diagnostic
/// to create.
/// A single-word category such as <c>error</c>, <c>warning</c>, or
/// <c>info</c>.
/// </param>
/// <param name="themeColor">
/// The diagnostic's theme color.
/// The color used to emphasize the diagnostic's kind and related
/// content.
/// </param>
/// <param name="title">
/// The contents of the diagnostic's title.
/// The short headline that appears in the diagnostic header after the
/// kind.
/// </param>
/// <param name="message">
/// The diagnostic's body, which may contain explanatory text,
/// highlighted source code, or other markup.
/// </param>
/// <param name="message"></param>
public Diagnostic(
MarkupNode origin,
string kind,
Expand All @@ -42,16 +50,15 @@ public Diagnostic(
}

/// <summary>
/// Gets the origin of this diagnostic: the line of code
/// or application that causes the diagnostic to be
/// issued.
/// Gets the origin of this diagnostic, typically a source reference or
/// application name that appears at the start of the header.
/// </summary>
/// <returns>The origin of the diagnostic.</returns>
public MarkupNode Origin { get; private set; }

/// <summary>
/// Gets a (single-word) description of the kind of diagnostic
/// this instance is.
/// Gets the single-word category of this diagnostic, for example
/// <c>error</c> or <c>warning</c>.
/// </summary>
/// <returns>The kind of diagnostic.</returns>
public string Kind { get; private set; }
Expand All @@ -74,7 +81,10 @@ public Diagnostic(
/// <returns>The message node.</returns>
public MarkupNode Message { get; private set; }

/// <inheritdoc/>
/// <summary>
/// Gets a default rendering of this diagnostic as a bold header
/// followed by its message body.
/// </summary>
public override MarkupNode Fallback
{
get
Expand Down Expand Up @@ -122,4 +132,4 @@ public override MarkupNode Map(Func<MarkupNode, MarkupNode> mapping)
}
}
}
}
}
36 changes: 22 additions & 14 deletions Pixie/Transforms/DiagnosticExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,30 @@
namespace Pixie.Transforms
{
/// <summary>
/// A transformation that turns log entries into diagnostics.
/// A transformation that upgrades plain log entries into
/// compiler-style diagnostics.
/// It looks for a title, text, and highlighted source in the markup tree
/// and uses that information to construct a diagnostic header and origin.
/// </summary>
public sealed class DiagnosticExtractor
{
/// <summary>
/// Creates a diagnostic extractor.
/// Creates a diagnostic extractor with defaults to use when the source
/// markup does not already provide a complete diagnostic.
/// </summary>
/// <param name="defaultOrigin">
/// The default origin to use, for when a log entry
/// does not specify an origin.
/// The fallback origin to use when a log entry does not contain a
/// source reference.
/// </param>
/// <param name="defaultKind">
/// The kind of diagnostic to provide.
/// The fallback diagnostic kind, such as <c>error</c> or
/// <c>warning</c>.
/// </param>
/// <param name="defaultThemeColor">
/// The diagnostic theme color.
/// The fallback theme color to use for the resulting diagnostic.
/// </param>
/// <param name="defaultTitle">
/// The diagnostic title.
/// The fallback title to use when the log entry does not contain one.
/// </param>
public DiagnosticExtractor(
MarkupNode defaultOrigin,
Expand Down Expand Up @@ -63,10 +68,11 @@ public DiagnosticExtractor(
public MarkupNode DefaultTitle { get; private set; }

/// <summary>
/// Transforms a markup tree to include a diagnostic.
/// Wraps a markup tree in a <see cref="Diagnostic"/> unless it already
/// contains one.
/// </summary>
/// <param name="tree">The tree to transform.</param>
/// <returns>A transformed tree.</returns>
/// <returns>A diagnostic node or the original diagnostic markup.</returns>
public MarkupNode Transform(MarkupNode tree)
{
var visitor = new DiagnosticExtractingVisitor(this);
Expand All @@ -92,13 +98,15 @@ public MarkupNode Transform(MarkupNode tree)
};

/// <summary>
/// Transforms a log entry to include a diagnostic.
/// Wraps a log entry in a <see cref="Diagnostic"/> using defaults based
/// on the entry's severity.
/// </summary>
/// <param name="entry">The log entry to transform.</param>
/// <param name="defaultOrigin">
/// The default origin of a diagnostic. This is typically the
/// name of the application.</param>
/// <returns>A transformed log entry.</returns>
/// The fallback diagnostic origin. This is typically the name of the
/// application.
/// </param>
/// <returns>A log entry whose contents are diagnostic markup.</returns>
public static LogEntry Transform(LogEntry entry, MarkupNode defaultOrigin)
{
var extractor = new DiagnosticExtractor(
Expand Down Expand Up @@ -203,4 +211,4 @@ public MarkupNode Transform(MarkupNode node)
}
}
}
}
}
46 changes: 46 additions & 0 deletions Pixie/Transforms/LogExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Pixie.Markup;

namespace Pixie.Transforms
{
/// <summary>
/// Convenience helpers for decorating logs with common transforms.
/// </summary>
public static class LogExtensions
{
/// <summary>
/// Wraps a log so that every entry is first converted to a
/// compiler-style diagnostic.
/// This is the easiest way to get headers such as
/// <c>file.cs:12:4: error: expected expression</c> without calling
/// <see cref="DiagnosticExtractor.Transform(LogEntry, MarkupNode)"/>
/// at each call site.
/// </summary>
/// <param name="log">The log to wrap.</param>
/// <param name="defaultOrigin">
/// The fallback origin to use when a log entry does not already
/// contain a source reference.
/// </param>
/// <returns>A log that emits diagnostic-formatted entries.</returns>
public static ILog WithDiagnostics(this ILog log, MarkupNode defaultOrigin)
{
return new TransformLog(
log,
entry => DiagnosticExtractor.Transform(entry, defaultOrigin));
}

/// <summary>
/// Wraps a log so that every entry is first converted to a
/// compiler-style diagnostic.
/// </summary>
/// <param name="log">The log to wrap.</param>
/// <param name="defaultOrigin">
/// The fallback origin to use when a log entry does not already
/// contain a source reference.
/// </param>
/// <returns>A log that emits diagnostic-formatted entries.</returns>
public static ILog WithDiagnostics(this ILog log, string defaultOrigin)
{
return log.WithDiagnostics(new Text(defaultOrigin));
}
}
}
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ dotnet add package Pixie.Loyc

### Caret diagnostics

Pixie has built-in support for caret diagnostics. It can highlight a source region, emphasize the most relevant span, and render line numbers with surrounding context.
Pixie has built-in support for compiler-style diagnostics. In Pixie, a diagnostic is a message with:

* an origin, such as a file location or application name,
* a kind, such as `error` or `warning`,
* a short title, and
* an optional body with extra context, such as highlighted source code.

That means Pixie can render both the diagnostic header, like `code.cs:3:5: error: expected token`, and the caret-highlighted snippet underneath it.

![Diagnostic](https://raw.githubusercontent.com/jonathanvdc/Pixie/master/docs/img/caret.svg)

Expand Down Expand Up @@ -190,14 +197,21 @@ To see a more complete example, check [Examples/PrintHelp/Program.cs](Examples/P

Pixie's diagnostic model is especially useful when you already know the source span you want to highlight.

There are two layers here:

* `HighlightedSource` renders a code snippet with line numbers and caret/squiggle markers.
* `WithDiagnostics(...)` wraps log entries as full diagnostics so they also get a header like `code.cs:3:5: error: Expected constructor name`.

In practice, most applications want both, so the usual setup is to enable diagnostics once on the log and then log `HighlightedSource` nodes normally.

```cs
using Pixie;
using Pixie.Code;
using Pixie.Markup;
using Pixie.Terminal;
using Pixie.Transforms;

var log = TerminalLog.Acquire();
var log = TerminalLog.Acquire().WithDiagnostics("program");

const string source = "public static class Program\n{\n public Program()\n { }\n}";
var document = new StringDocument("code.cs", source);
Expand All @@ -206,17 +220,15 @@ var nameOffset = source.IndexOf("Program()");
var focusRegion = new SourceRegion(
new SourceSpan(document, nameOffset, "Program".Length));

var entry = new LogEntry(
log.Log(new LogEntry(
Severity.Error,
"Expected constructor name",
new HighlightedSource(focusRegion, focusRegion));

log.Log(DiagnosticExtractor.Transform(entry, new Text("program")));
new HighlightedSource(focusRegion, focusRegion)));
```

`HighlightedSource` renders the numbered source snippet and caret highlight. The `code.cs:line:column: error: ...` header is added by `DiagnosticExtractor.Transform(...)`, which wraps the entry in a diagnostic using the highlighted source span as the origin.
This works because `log.WithDiagnostics("program")` converts each `LogEntry` into a diagnostic before rendering it. When the entry contains a `HighlightedSource`, Pixie uses that source span as the diagnostic origin, which is what makes the filename and line/column information appear in the header.

If you log a raw `LogEntry` with `new HighlightedSource(...)`, Pixie will render the snippet but not the document identifier header.
If you log a raw `LogEntry` with `new HighlightedSource(...)` but do not wrap the log with `WithDiagnostics(...)`, Pixie will still render the snippet and caret, but it will not render the `code.cs:line:column: error: ...` header.

For a fuller version with transforms and custom renderer configuration, see [Examples/CaretDiagnostics/Program.cs](Examples/CaretDiagnostics/Program.cs).

Expand Down
13 changes: 13 additions & 0 deletions Tests/LogBehaviorTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NUnit.Framework;
using Pixie.Transforms;

namespace Pixie.Tests
{
Expand Down Expand Up @@ -31,5 +32,17 @@ public void TransformLogAppliesTransformsInOrder()
StringAssert.Contains("first", RenderTests.Render(sink.RecordedEntries[0].Contents));
StringAssert.Contains("second", RenderTests.Render(sink.RecordedEntries[0].Contents));
}

[Test]
public void WithDiagnosticsWrapsEntriesInDiagnosticTransform()
{
var sink = new RecordingLog();
var log = sink.WithDiagnostics("program");

log.Log(new LogEntry(Severity.Error, "oops"));

var rendered = RenderTests.Render(sink.RecordedEntries[0].Contents);
StringAssert.Contains("program: error: oops", rendered);
}
}
}
12 changes: 3 additions & 9 deletions Tests/TestEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@ namespace Pixie.Tests
{
public static class TestEnvironment
{
public static readonly ILog GlobalLog = new TransformLog(
new TestLog(
public static readonly ILog GlobalLog = new TestLog(
new[] { Severity.Error },
Pixie.Terminal.TerminalLog.Acquire()),
MakeDiagnostic);

private static LogEntry MakeDiagnostic(LogEntry entry)
{
return DiagnosticExtractor.Transform(entry, "program");
}
Pixie.Terminal.TerminalLog.Acquire())
.WithDiagnostics("program");
}
}
Loading