diff --git a/Examples/CaretDiagnostics/Program.cs b/Examples/CaretDiagnostics/Program.cs index 73e53c3..69b74d5 100644 --- a/Examples/CaretDiagnostics/Program.cs +++ b/Examples/CaretDiagnostics/Program.cs @@ -32,12 +32,7 @@ public static void Main(string[] args) new HighlightedSourceRenderer(5) }); - log = new TransformLog( - log, - new Func[] - { - MakeDiagnostic - }); + log = log.WithDiagnostics("program"); var doc = new StringDocument("code.cs", SourceCode); var ctorStartOffset = SourceCode.IndexOf("public Program()"); @@ -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() diff --git a/Pixie/Markup/Diagnostic.cs b/Pixie/Markup/Diagnostic.cs index aeaac9e..77c13d6 100644 --- a/Pixie/Markup/Diagnostic.cs +++ b/Pixie/Markup/Diagnostic.cs @@ -3,30 +3,38 @@ namespace Pixie.Markup { /// - /// 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 + /// file.cs:12:4: error: expected expression followed by additional + /// context such as highlighted source code. /// public sealed class Diagnostic : MarkupNode { /// - /// Creates a diagnostic. + /// Creates a diagnostic with a header and message body. /// /// - /// 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. /// /// - /// A single-word description of the kind of diagnostic - /// to create. + /// A single-word category such as error, warning, or + /// info. /// /// - /// The diagnostic's theme color. + /// The color used to emphasize the diagnostic's kind and related + /// content. /// /// - /// The contents of the diagnostic's title. + /// The short headline that appears in the diagnostic header after the + /// kind. + /// + /// + /// The diagnostic's body, which may contain explanatory text, + /// highlighted source code, or other markup. /// - /// public Diagnostic( MarkupNode origin, string kind, @@ -42,16 +50,15 @@ public Diagnostic( } /// - /// 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. /// /// The origin of the diagnostic. public MarkupNode Origin { get; private set; } /// - /// Gets a (single-word) description of the kind of diagnostic - /// this instance is. + /// Gets the single-word category of this diagnostic, for example + /// error or warning. /// /// The kind of diagnostic. public string Kind { get; private set; } @@ -74,7 +81,10 @@ public Diagnostic( /// The message node. public MarkupNode Message { get; private set; } - /// + /// + /// Gets a default rendering of this diagnostic as a bold header + /// followed by its message body. + /// public override MarkupNode Fallback { get @@ -122,4 +132,4 @@ public override MarkupNode Map(Func mapping) } } } -} \ No newline at end of file +} diff --git a/Pixie/Transforms/DiagnosticExtractor.cs b/Pixie/Transforms/DiagnosticExtractor.cs index 97b37ec..9d932e1 100644 --- a/Pixie/Transforms/DiagnosticExtractor.cs +++ b/Pixie/Transforms/DiagnosticExtractor.cs @@ -5,25 +5,30 @@ namespace Pixie.Transforms { /// - /// 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. /// public sealed class DiagnosticExtractor { /// - /// Creates a diagnostic extractor. + /// Creates a diagnostic extractor with defaults to use when the source + /// markup does not already provide a complete diagnostic. /// /// - /// 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. /// /// - /// The kind of diagnostic to provide. + /// The fallback diagnostic kind, such as error or + /// warning. /// /// - /// The diagnostic theme color. + /// The fallback theme color to use for the resulting diagnostic. /// /// - /// The diagnostic title. + /// The fallback title to use when the log entry does not contain one. /// public DiagnosticExtractor( MarkupNode defaultOrigin, @@ -63,10 +68,11 @@ public DiagnosticExtractor( public MarkupNode DefaultTitle { get; private set; } /// - /// Transforms a markup tree to include a diagnostic. + /// Wraps a markup tree in a unless it already + /// contains one. /// /// The tree to transform. - /// A transformed tree. + /// A diagnostic node or the original diagnostic markup. public MarkupNode Transform(MarkupNode tree) { var visitor = new DiagnosticExtractingVisitor(this); @@ -92,13 +98,15 @@ public MarkupNode Transform(MarkupNode tree) }; /// - /// Transforms a log entry to include a diagnostic. + /// Wraps a log entry in a using defaults based + /// on the entry's severity. /// /// The log entry to transform. /// - /// The default origin of a diagnostic. This is typically the - /// name of the application. - /// A transformed log entry. + /// The fallback diagnostic origin. This is typically the name of the + /// application. + /// + /// A log entry whose contents are diagnostic markup. public static LogEntry Transform(LogEntry entry, MarkupNode defaultOrigin) { var extractor = new DiagnosticExtractor( @@ -203,4 +211,4 @@ public MarkupNode Transform(MarkupNode node) } } } -} \ No newline at end of file +} diff --git a/Pixie/Transforms/LogExtensions.cs b/Pixie/Transforms/LogExtensions.cs new file mode 100644 index 0000000..e1eb619 --- /dev/null +++ b/Pixie/Transforms/LogExtensions.cs @@ -0,0 +1,46 @@ +using Pixie.Markup; + +namespace Pixie.Transforms +{ + /// + /// Convenience helpers for decorating logs with common transforms. + /// + public static class LogExtensions + { + /// + /// 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 + /// file.cs:12:4: error: expected expression without calling + /// + /// at each call site. + /// + /// The log to wrap. + /// + /// The fallback origin to use when a log entry does not already + /// contain a source reference. + /// + /// A log that emits diagnostic-formatted entries. + public static ILog WithDiagnostics(this ILog log, MarkupNode defaultOrigin) + { + return new TransformLog( + log, + entry => DiagnosticExtractor.Transform(entry, defaultOrigin)); + } + + /// + /// Wraps a log so that every entry is first converted to a + /// compiler-style diagnostic. + /// + /// The log to wrap. + /// + /// The fallback origin to use when a log entry does not already + /// contain a source reference. + /// + /// A log that emits diagnostic-formatted entries. + public static ILog WithDiagnostics(this ILog log, string defaultOrigin) + { + return log.WithDiagnostics(new Text(defaultOrigin)); + } + } +} diff --git a/README.md b/README.md index c907e4e..d23f835 100644 --- a/README.md +++ b/README.md @@ -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) @@ -190,6 +197,13 @@ 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; @@ -197,7 +211,7 @@ 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); @@ -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). diff --git a/Tests/LogBehaviorTests.cs b/Tests/LogBehaviorTests.cs index f5f5d2c..15e0959 100644 --- a/Tests/LogBehaviorTests.cs +++ b/Tests/LogBehaviorTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Pixie.Transforms; namespace Pixie.Tests { @@ -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); + } } } diff --git a/Tests/TestEnvironment.cs b/Tests/TestEnvironment.cs index 67232c8..fb4a4a2 100644 --- a/Tests/TestEnvironment.cs +++ b/Tests/TestEnvironment.cs @@ -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"); } }