Import codespan-reporting 0.11.1 upstream upstream/0.11.1
authorDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 28 Apr 2025 06:34:45 +0000 (15:34 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 28 Apr 2025 06:34:45 +0000 (15:34 +0900)
111 files changed:
.cargo_vcs_info.json [new file with mode: 0644]
CHANGELOG.md [new file with mode: 0644]
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
Cargo.toml.orig [new file with mode: 0644]
examples/custom_files.rs [new file with mode: 0644]
examples/peg_calculator.rs [new file with mode: 0644]
examples/readme_preview.rs [new file with mode: 0644]
examples/reusable_diagnostic.rs [new file with mode: 0644]
examples/term.rs [new file with mode: 0644]
src/diagnostic.rs [new file with mode: 0644]
src/files.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/term.rs [new file with mode: 0644]
src/term/config.rs [new file with mode: 0644]
src/term/renderer.rs [new file with mode: 0644]
src/term/views.rs [new file with mode: 0644]
tests/snapshots/term__empty__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__empty__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__empty__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__empty__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__empty__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__empty__short_color.snap [new file with mode: 0644]
tests/snapshots/term__empty__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__empty_ranges__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__empty_ranges__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__empty_ranges__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__empty_ranges__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__empty_ranges__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__empty_ranges__short_color.snap [new file with mode: 0644]
tests/snapshots/term__empty_ranges__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__fizz_buzz__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__fizz_buzz__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__fizz_buzz__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__fizz_buzz__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__fizz_buzz__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__fizz_buzz__short_color.snap [new file with mode: 0644]
tests/snapshots/term__fizz_buzz__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__message__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__message__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message__short_color.snap [new file with mode: 0644]
tests/snapshots/term__message__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message_and_notes__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__message_and_notes__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message_and_notes__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message_and_notes__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__message_and_notes__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message_and_notes__short_color.snap [new file with mode: 0644]
tests/snapshots/term__message_and_notes__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message_errorcode__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message_errorcode__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__message_errorcode__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multifile__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__multifile__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multifile__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multifile__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__multifile__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multifile__short_color.snap [new file with mode: 0644]
tests/snapshots/term__multifile__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multiline_omit__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multiline_overlapping__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__multiline_overlapping__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multiline_overlapping__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multiline_overlapping__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__multiline_overlapping__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__multiline_overlapping__short_color.snap [new file with mode: 0644]
tests/snapshots/term__multiline_overlapping__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__overlapping__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__overlapping__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__overlapping__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__overlapping__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__overlapping__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__overlapping__short_color.snap [new file with mode: 0644]
tests/snapshots/term__overlapping__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__position_indicator__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__position_indicator__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__position_indicator__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__position_indicator__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__same_line__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__same_line__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__same_line__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__same_line__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__same_line__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__same_line__short_color.snap [new file with mode: 0644]
tests/snapshots/term__same_line__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__same_ranges__medium_color.snap [new file with mode: 0644]
tests/snapshots/term__same_ranges__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__same_ranges__rich_ascii_no_color.snap [new file with mode: 0644]
tests/snapshots/term__same_ranges__rich_color.snap [new file with mode: 0644]
tests/snapshots/term__same_ranges__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__same_ranges__short_color.snap [new file with mode: 0644]
tests/snapshots/term__same_ranges__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__tab_columns__tab_width_2_no_color.snap [new file with mode: 0644]
tests/snapshots/term__tab_columns__tab_width_3_no_color.snap [new file with mode: 0644]
tests/snapshots/term__tab_columns__tab_width_6_no_color.snap [new file with mode: 0644]
tests/snapshots/term__tab_columns__tab_width_default_no_color.snap [new file with mode: 0644]
tests/snapshots/term__tabbed__tab_width_3_no_color.snap [new file with mode: 0644]
tests/snapshots/term__tabbed__tab_width_6_no_color.snap [new file with mode: 0644]
tests/snapshots/term__tabbed__tab_width_default_no_color.snap [new file with mode: 0644]
tests/snapshots/term__unicode__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__unicode__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__unicode__short_no_color.snap [new file with mode: 0644]
tests/snapshots/term__unicode_spans__medium_no_color.snap [new file with mode: 0644]
tests/snapshots/term__unicode_spans__rich_no_color.snap [new file with mode: 0644]
tests/snapshots/term__unicode_spans__short_no_color.snap [new file with mode: 0644]
tests/support/color_buffer.rs [new file with mode: 0644]
tests/support/mod.rs [new file with mode: 0644]
tests/term.rs [new file with mode: 0644]

diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644 (file)
index 0000000..b70187f
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "git": {
+    "sha1": "fd389a13f5bb6d625b71e2e4694b26e127f393f9"
+  }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644 (file)
index 0000000..68109e0
--- /dev/null
@@ -0,0 +1,408 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [0.11.1] - 2021-01-18
+
+### Added
+
+-   Add `Chars::{box_drawing, ascii}` functions, the latter supporting a rustc-style of
+    output that only uses ASCII characters (not above U+007F) for use cases that do not allow
+    for box drawing characters, e.g. terminals that do not support them.
+
+### Changed
+
+-   `Diagnostic::with_labels` and `Diagnostic::with_notes` now append additional
+    labels rather tan overwriting them, meaning that the documentation and behaviour match
+    more closely. The behaviour will only differ if you call the same builder methods
+    multiple times. If you call every builder method once only, nothing should change.
+-   `config::Chars::snippet_start` is now a String instead of a single `char`.
+
+## [0.11.0] - 2020-11-30
+
+There is now a [code of conduct](https://github.com/brendanzab/codespan/blob/master/CODE_OF_CONDUCT.md)
+and a [contributing guide](https://github.com/brendanzab/codespan/blob/master/CONTRIBUTING.md).
+
+Some versions were skipped to sync up with the `codespan-lsp` crate. The release
+process has been changed so this should not happen again.
+
+### Added
+
+-   If a label spans over multiple lines, not all lines are rendered.
+    The number of lines rendered at beginning and end is configurable separately.
+-   There is now a custom error type.
+-   There now is a medium rendering mode that is like the short rendering mode
+    but also shows notes from the diagnostic.
+-   `PartialEq` and `Eq` implementations for the `diagnostic::{Diagnostic, Label, Severity}` types.
+
+### Changed
+
+-   All errors now use the error type `codespan_reporting::file::Error`.
+    This type also replaces the custom error type for `codespan-lsp`.
+
+### Fixed
+
+-   Empty error codes are not rendered.
+-   The locus ("location of the diagnostic") is now computed so it is always at the first
+    primary label, or at the first secondary label if no primary labels are available.
+-   All `unwrap`s outside of tests and examples have been removed.
+-   Some internal improvements, including various code style improvements by using Clippy.
+-   Improved documentation, also mentioning how the ordering of labels is handled.
+
+## [0.9.5] - 2020-06-24
+
+### Changed
+
+-   Sections of source code that are marked with primary labels are now rendered
+    using the primary highlight color.
+-   Tab stops are now rendered properly.
+
+    We used to just render `\t` characters in source snippets with the same
+    number of spaces.
+
+    <details>
+    <summary>Example</summary>
+
+    For example, when rendering with a tab width of `3` we
+    would print:
+
+    ```text
+    warning: tab test
+      ┌─ tab_columns:1:2
+      │
+    1 │    hello
+      │    ^^^^^
+    2 │ ∙   hello
+      │     ^^^^^
+    3 │ ∙∙   hello
+      │      ^^^^^
+    4 │ ∙∙∙   hello
+      │       ^^^^^
+    5 │ ∙∙∙∙   hello
+      │        ^^^^^
+    6 │ ∙∙∙∙∙   hello
+      │         ^^^^^
+    7 │ ∙∙∙∙∙∙   hello
+      │          ^^^^^
+    ```
+
+    Now we properly take into account the column of the tab character:
+
+    ```text
+    warning: tab test
+      ┌─ tab_columns:1:2
+      │
+    1 │    hello
+      │    ^^^^^
+    2 │ ∙  hello
+      │    ^^^^^
+    3 │ ∙∙ hello
+      │    ^^^^^
+    4 │ ∙∙∙   hello
+      │       ^^^^^
+    5 │ ∙∙∙∙  hello
+      │       ^^^^^
+    6 │ ∙∙∙∙∙ hello
+      │       ^^^^^
+    7 │ ∙∙∙∙∙∙   hello
+      │          ^^^^^
+    ```
+
+    </details>
+
+## [0.9.4] - 2020-05-18
+
+### Changed
+
+-   We have made the caret rendering easier to read when there are multiple
+    labels on the same line. We also avoid printing trailing borders on the
+    final source source snippet if no notes are present.
+
+    <details>
+    <summary>Example</summary>
+
+    Instead of this:
+
+    ```text
+       ┌─ one_line.rs:3:5
+       │
+     3 │     v.push(v.pop().unwrap());
+       │     - first borrow later used by call
+       │       ---- first mutable borrow occurs here
+       │            ^ second mutable borrow occurs here
+       │
+    ```
+
+    …we now render the following:
+
+    ```text
+       ┌─ one_line.rs:3:5
+       │
+     3 │     v.push(v.pop().unwrap());
+       │     - ---- ^ second mutable borrow occurs here
+       │     │ │
+       │     │ first mutable borrow occurs here
+       │     first borrow later used by call
+    ```
+
+    </details>
+
+### Fixed
+
+-   Diagnostic rendering no longer panics if label ranges are between UTF-8
+    character boundaries.
+
+## [0.9.3] - 2020-04-29
+
+### Changed
+
+-   Some panics were fixed when invalid unicode boundaries are supplied.
+-   Labels that marked the same span were originally rendered in reverse order.
+    This was a mistake! We've now fixed this.
+
+    <details>
+    <summary>Example</summary>
+
+    For example, this diagnostic:
+
+    ```text
+       ┌─ same_range:1:7
+       │
+     1 │ ::S { }
+       │     - Expected '('
+       │     ^ Unexpected '{'
+       │
+    ```
+
+    …will now be rendered as:
+
+    ```text
+       ┌─ same_range:1:7
+       │
+     1 │ ::S { }
+       │     ^ Unexpected '{'
+       │     - Expected '('
+       │
+    ```
+
+    </details>
+
+-   We've reduced the prominence of the 'locus' on source snippets by
+    simplifying the border and reducing the spacing around it. This is to help
+    focus attention on the underlined source snippet and error messages, rather
+    than the location, which should be a secondary focus.
+
+    <details>
+    <summary>Example</summary>
+
+    For example we originally rendered this:
+
+    ```text
+    error: unknown builtin: `NATRAL`
+
+       ┌── Data/Nat.fun:7:13 ───
+       │
+     7 │ {-# BUILTIN NATRAL Nat #-}
+       │             ^^^^^^ unknown builtin
+       │
+       = there is a builtin with a similar name: `NATURAL`
+
+    ```
+
+    …and now we render this:
+
+    ```text
+    error: unknown builtin: `NATRAL`
+      ┌─ Data/Nat.fun:7:13
+      │
+    7 │ {-# BUILTIN NATRAL Nat #-}
+      │             ^^^^^^ unknown builtin
+      │
+      = there is a builtin with a similar name: `NATURAL`
+
+    ```
+
+    </details>
+
+## [0.9.2] - 2020-03-29
+
+### Changed
+
+-   Render overlapping multiline marks on the same lines of source code.
+
+    <details>
+    <summary>Example</summary>
+
+    For example:
+
+    ```text
+    error[E0308]: match arms have incompatible types
+
+       ┌── codespan/src/file.rs:1:9 ───
+       │
+     1 │ ╭         match line_index.compare(self.last_line_index()) {
+     2 │ │             Ordering::Less => Ok(self.line_starts()[line_index.to_usize()]),
+     3 │ │             Ordering::Equal => Ok(self.source_span().end()),
+     4 │ │             Ordering::Greater => LineIndexOutOfBoundsError {
+     5 │ │                 given: line_index,
+     6 │ │                 max: self.last_line_index(),
+     7 │ │             },
+     8 │ │         }
+       │ ╰─────────' `match` arms have incompatible types
+       ·
+     2 │               Ordering::Less => Ok(self.line_starts()[line_index.to_usize()]),
+       │                                 --------------------------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+     3 │               Ordering::Equal => Ok(self.source_span().end()),
+       │                                  ---------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+     4 │               Ordering::Greater => LineIndexOutOfBoundsError {
+       │ ╭──────────────────────────────────^
+     5 │ │                 given: line_index,
+     6 │ │                 max: self.last_line_index(),
+     7 │ │             },
+       │ ╰─────────────^ expected enum `Result`, found struct `LineIndexOutOfBoundsError`
+       │
+       = expected type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+            found type `LineIndexOutOfBoundsError`
+    ```
+
+    …is now rendered as:
+
+    ```text
+    error[E0308]: match arms have incompatible types
+
+       ┌── codespan/src/file.rs:1:9 ───
+       │
+     1 │   ╭         match line_index.compare(self.last_line_index()) {
+     2 │   │             Ordering::Less => Ok(self.line_starts()[line_index.to_usize()]),
+       │   │                               --------------------------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+     3 │   │             Ordering::Equal => Ok(self.source_span().end()),
+       │   │                                ---------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+     4 │   │             Ordering::Greater => LineIndexOutOfBoundsError {
+       │ ╭─│──────────────────────────────────^
+     5 │ │ │                 given: line_index,
+     6 │ │ │                 max: self.last_line_index(),
+     7 │ │ │             },
+       │ ╰─│─────────────^ expected enum `Result`, found struct `LineIndexOutOfBoundsError`
+     8 │   │         }
+       │   ╰─────────' `match` arms have incompatible types
+       │
+       = expected type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+            found type `LineIndexOutOfBoundsError`
+    ```
+
+    </details>
+
+## [0.9.1] - 2020-03-23
+
+### Added
+
+-   `codespan_reporting::diagnostic::Diagnostic` now implements `Debug`.
+
+### Changed
+
+-   Single-line labels are now rendered together, under the same source line.
+
+    <details>
+    <summary>Example</summary>
+
+    For example:
+
+    ```text
+       ┌── one_line.rs:3:5 ───
+       │
+     3 │     v.push(v.pop().unwrap());
+       │     - first borrow later used by call
+       ·
+     3 │     v.push(v.pop().unwrap());
+       │       ---- first mutable borrow occurs here
+       ·
+     3 │     v.push(v.pop().unwrap());
+       │            ^ second mutable borrow occurs here
+       │
+    ```
+
+    …is now rendered as:
+
+    ```text
+       ┌── one_line.rs:3:5 ───
+       │
+     3 │     v.push(v.pop().unwrap());
+       │     - first borrow later used by call
+       │       ---- first mutable borrow occurs here
+       │            ^ second mutable borrow occurs here
+       │
+    ```
+
+    </details>
+
+## [0.9.0] - 2020-03-11
+
+### Added
+
+-   The `codespan_reporting::files` module was added as a way to decouple
+    `codespan_reporting` from `codespan`.
+    -   `codespan_reporting::files::Files` allows users to implement custom file
+        databases that work with `codespan_reporting`. This should make it
+        easier to integrate with libraries like Salsa, and also makes it less
+        invasive to use `codespan_reporting` on existing projects.
+    -   `codespan_reporting::files::SimpleFile` is a simple implementation of
+        `codespan_reporting::files::Files` where only a single file is needed.
+    -   `codespan_reporting::files::SimpleFiles` is a simple implementation of
+        `codespan_reporting::files::Files` where multiple files are needed.
+
+### Changed
+
+-   The `codespan_reporting::diagnostic` module has been greatly revamped,
+    making the builder API format more nicely with rustfmt, and allowing for
+    multiple primary labels.
+-   The output of `codespan_reporting::term::emit` was improved,
+    with the following changes:
+    -   labels on consecutive lines no longer render breaks between them
+    -   source lines are rendered when there is only one line between labels
+    -   the inner gutter of code snippets is now aligned consistently
+    -   the outer gutter of consecutive code snippets are now aligned consistently
+-   `codespan_reporting::term::emit` now takes writers as a trait object (rather
+    than using static dispatch) in order to reduce coda bloat and improve
+    compile times.
+-   The field names in `codespan_reporting::term::Chars` were tweaked for
+    consistency.
+
+### Removed
+
+-   `codespan_reporting` no longer depends on `codespan`.
+    Note that `codespan` can _still_ be used with `codespan_reporting`,
+    as `codespan::Files` now implements `codespan_reporting::files::Files`.
+
+## [0.8.0] - 2020-02-24
+## [0.7.0] - 2020-01-06
+## [0.6.0] - 2019-12-18
+## [0.5.0] - 2019-10-02
+## [0.4.1] - 2019-08-25
+## [0.4.0] - 2019-08-22
+## [0.3.0] - 2019-05-01
+## [0.2.1] - 2019-02-26
+## [0.2.0] - 2018-10-11
+
+[Unreleased]: https://github.com/brendanzab/codespan/compare/v0.11.1...HEAD
+[0.11.1]: https://github.com/brendanzab/codespan/compare/v0.11.0..v0.11.1
+[0.11.0]: https://github.com/brendanzab/codespan/compare/v0.9.5...v0.11.0
+[0.9.5]: https://github.com/brendanzab/codespan/compare/v0.9.4...v0.9.5
+[0.9.4]: https://github.com/brendanzab/codespan/compare/v0.9.3...v0.9.4
+[0.9.3]: https://github.com/brendanzab/codespan/compare/v0.9.2...v0.9.3
+[0.9.2]: https://github.com/brendanzab/codespan/compare/v0.9.1...v0.9.2
+[0.9.1]: https://github.com/brendanzab/codespan/compare/v0.9.0...v0.9.1
+[0.9.0]: https://github.com/brendanzab/codespan/compare/v0.8.0...v0.9.0
+[0.8.0]: https://github.com/brendanzab/codespan/compare/v0.7.0...v0.8.0
+[0.7.0]: https://github.com/brendanzab/codespan/compare/v0.6.0...v0.7.0
+[0.6.0]: https://github.com/brendanzab/codespan/compare/v0.5.0...v0.6.0
+[0.5.0]: https://github.com/brendanzab/codespan/compare/v0.4.1...v0.5.0
+[0.4.1]: https://github.com/brendanzab/codespan/compare/v0.4.0...v0.4.1
+[0.4.0]: https://github.com/brendanzab/codespan/compare/v0.3.0...v0.4.0
+[0.3.0]: https://github.com/brendanzab/codespan/compare/v0.2.1...v0.3.0
+[0.2.1]: https://github.com/brendanzab/codespan/compare/v0.2.0...v0.2.1
+[0.2.0]: https://github.com/brendanzab/codespan/releases/tag/v0.2.0
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644 (file)
index 0000000..ff92d96
--- /dev/null
@@ -0,0 +1,554 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "cc"
+version = "1.0.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+dependencies = [
+ "anyhow",
+ "insta",
+ "lazy_static",
+ "peg",
+ "rustyline",
+ "serde",
+ "structopt",
+ "termcolor",
+ "unicode-width",
+ "unindent",
+]
+
+[[package]]
+name = "console"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc80946b3480f421c2f17ed1cb841753a371c7c5104f51d507e13f532c856aa"
+dependencies = [
+ "encode_unicode",
+ "lazy_static",
+ "libc",
+ "terminal_size",
+ "winapi",
+]
+
+[[package]]
+name = "dirs-next"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf36e65a80337bea855cd4ef9b8401ffce06a7baedf2e85ec467b1ac3f6e82b6"
+dependencies = [
+ "cfg-if 1.0.0",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dtoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
+
+[[package]]
+name = "encode_unicode"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+
+[[package]]
+name = "getrandom"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "heck"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "insta"
+version = "1.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd354a2c8c8083d58414597a4ecada1984f9b82ea7e87eeabddc869eaf120992"
+dependencies = [
+ "console",
+ "lazy_static",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "similar",
+ "uuid",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c"
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+
+[[package]]
+name = "nix"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if 0.1.10",
+ "libc",
+]
+
+[[package]]
+name = "peg"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f76678828272f177ac33b7e2ac2e3e73cc6c1cd1e3e387928aa69562fa51367"
+dependencies = [
+ "peg-macros",
+ "peg-runtime",
+]
+
+[[package]]
+name = "peg-macros"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "636d60acf97633e48d266d7415a9355d4389cea327a193f87df395d88cd2b14d"
+dependencies = [
+ "peg-runtime",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "peg-runtime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555b1514d2d99d78150d3c799d4c357a3e2c2a8062cd108e93a06d9057629c5"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+]
+
+[[package]]
+name = "rustyline"
+version = "6.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0d5e7b0219a3eadd5439498525d4765c59b7c993ef0c12244865cd2d988413"
+dependencies = [
+ "cfg-if 0.1.10",
+ "dirs-next",
+ "libc",
+ "log",
+ "memchr",
+ "nix",
+ "scopeguard",
+ "unicode-segmentation",
+ "unicode-width",
+ "utf8parse",
+ "winapi",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "serde"
+version = "1.0.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.8.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
+dependencies = [
+ "dtoa",
+ "linked-hash-map",
+ "serde",
+ "yaml-rust",
+]
+
+[[package]]
+name = "similar"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a04629d2e8ecdcf30e0188e3699ed6d50d5750d0219db146a790065fe92a897"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "structopt"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c"
+dependencies = [
+ "clap",
+ "lazy_static",
+ "structopt-derive",
+]
+
+[[package]]
+name = "structopt-derive"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
+name = "unindent"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
+
+[[package]]
+name = "uuid"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..5a3d588
--- /dev/null
@@ -0,0 +1,58 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "codespan-reporting"
+version = "0.11.1"
+authors = ["Brendan Zabarauskas <bjzaba@yahoo.com.au>"]
+exclude = ["assets/**"]
+description = "Beautiful diagnostic reporting for text-based programming languages"
+homepage = "https://github.com/brendanzab/codespan"
+documentation = "https://docs.rs/codespan-reporting"
+readme = "../README.md"
+license = "Apache-2.0"
+repository = "https://github.com/brendanzab/codespan"
+[dependencies.serde]
+version = "1"
+features = ["derive"]
+optional = true
+
+[dependencies.termcolor]
+version = "1"
+
+[dependencies.unicode-width]
+version = "0.1"
+[dev-dependencies.anyhow]
+version = "1"
+
+[dev-dependencies.insta]
+version = "1.6.3"
+
+[dev-dependencies.lazy_static]
+version = "1.4"
+
+[dev-dependencies.peg]
+version = "0.6"
+
+[dev-dependencies.rustyline]
+version = "6"
+
+[dev-dependencies.structopt]
+version = "0.3"
+
+[dev-dependencies.unindent]
+version = "0.1"
+
+[features]
+ascii-only = []
+serialization = ["serde", "serde/rc"]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644 (file)
index 0000000..50ad3d8
--- /dev/null
@@ -0,0 +1,30 @@
+[package]
+name = "codespan-reporting"
+version = "0.11.1"
+readme = "../README.md"
+license = "Apache-2.0"
+authors = ["Brendan Zabarauskas <bjzaba@yahoo.com.au>"]
+description = "Beautiful diagnostic reporting for text-based programming languages"
+homepage = "https://github.com/brendanzab/codespan"
+repository = "https://github.com/brendanzab/codespan"
+documentation = "https://docs.rs/codespan-reporting"
+exclude = ["assets/**"]
+edition = "2018"
+
+[dependencies]
+serde = { version = "1", optional = true, features = ["derive"] }
+termcolor = "1"
+unicode-width = "0.1"
+
+[dev-dependencies]
+anyhow = "1"
+insta = "1.6.3"
+lazy_static = "1.4"
+peg = "0.6"
+rustyline = "6"
+structopt = "0.3"
+unindent = "0.1"
+
+[features]
+serialization = ["serde", "serde/rc"]
+ascii-only = []
diff --git a/examples/custom_files.rs b/examples/custom_files.rs
new file mode 100644 (file)
index 0000000..dbbacda
--- /dev/null
@@ -0,0 +1,197 @@
+//! An example that shows how to implement a simple custom file database.
+//! The database uses 32-bit file-ids, which could be useful for optimizing
+//! memory usage.
+//!
+//! To run this example, execute the following command from the top level of
+//! this repository:
+//!
+//! ```sh
+//! cargo run --example custom_files
+//! ```
+
+use codespan_reporting::diagnostic::{Diagnostic, Label};
+use codespan_reporting::term;
+use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
+use std::ops::Range;
+
+fn main() -> anyhow::Result<()> {
+    let mut files = files::Files::new();
+
+    let file_id0 = files.add("0.greeting", "hello world!").unwrap();
+    let file_id1 = files.add("1.greeting", "bye world").unwrap();
+
+    let messages = vec![
+        Message::UnwantedGreetings {
+            greetings: vec![(file_id0, 0..5), (file_id1, 0..3)],
+        },
+        Message::OverTheTopExclamations {
+            exclamations: vec![(file_id0, 11..12)],
+        },
+    ];
+
+    let writer = StandardStream::stderr(ColorChoice::Always);
+    let config = term::Config::default();
+    for message in &messages {
+        let writer = &mut writer.lock();
+        term::emit(writer, &config, &files, &message.to_diagnostic())?;
+    }
+
+    Ok(())
+}
+
+/// A module containing the file implementation
+mod files {
+    use codespan_reporting::files;
+    use std::ops::Range;
+
+    /// A file that is backed by an `Arc<String>`.
+    #[derive(Debug, Clone)]
+    struct File {
+        /// The name of the file.
+        name: String,
+        /// The source code of the file.
+        source: String,
+        /// The starting byte indices in the source code.
+        line_starts: Vec<usize>,
+    }
+
+    impl File {
+        fn line_start(&self, line_index: usize) -> Result<usize, files::Error> {
+            use std::cmp::Ordering;
+
+            match line_index.cmp(&self.line_starts.len()) {
+                Ordering::Less => Ok(self
+                    .line_starts
+                    .get(line_index)
+                    .expect("failed despite previous check")
+                    .clone()),
+                Ordering::Equal => Ok(self.source.len()),
+                Ordering::Greater => Err(files::Error::LineTooLarge {
+                    given: line_index,
+                    max: self.line_starts.len() - 1,
+                }),
+            }
+        }
+    }
+
+    /// An opaque file identifier.
+    #[derive(Copy, Clone, PartialEq, Eq)]
+    pub struct FileId(u32);
+
+    #[derive(Debug, Clone)]
+    pub struct Files {
+        files: Vec<File>,
+    }
+
+    impl Files {
+        /// Create a new files database.
+        pub fn new() -> Files {
+            Files { files: Vec::new() }
+        }
+
+        /// Add a file to the database, returning the handle that can be used to
+        /// refer to it again.
+        pub fn add(
+            &mut self,
+            name: impl Into<String>,
+            source: impl Into<String>,
+        ) -> Option<FileId> {
+            use std::convert::TryFrom;
+
+            let file_id = FileId(u32::try_from(self.files.len()).ok()?);
+            let name = name.into();
+            let source = source.into();
+            let line_starts = files::line_starts(&source).collect();
+
+            self.files.push(File {
+                name,
+                line_starts,
+                source,
+            });
+
+            Some(file_id)
+        }
+
+        /// Get the file corresponding to the given id.
+        fn get(&self, file_id: FileId) -> Result<&File, files::Error> {
+            self.files
+                .get(file_id.0 as usize)
+                .ok_or(files::Error::FileMissing)
+        }
+    }
+
+    impl<'files> files::Files<'files> for Files {
+        type FileId = FileId;
+        type Name = &'files str;
+        type Source = &'files str;
+
+        fn name(&self, file_id: FileId) -> Result<&str, files::Error> {
+            Ok(self.get(file_id)?.name.as_ref())
+        }
+
+        fn source(&self, file_id: FileId) -> Result<&str, files::Error> {
+            Ok(&self.get(file_id)?.source)
+        }
+
+        fn line_index(&self, file_id: FileId, byte_index: usize) -> Result<usize, files::Error> {
+            self.get(file_id)?
+                .line_starts
+                .binary_search(&byte_index)
+                .or_else(|next_line| Ok(next_line - 1))
+        }
+
+        fn line_range(
+            &self,
+            file_id: FileId,
+            line_index: usize,
+        ) -> Result<Range<usize>, files::Error> {
+            let file = self.get(file_id)?;
+            let line_start = file.line_start(line_index)?;
+            let next_line_start = file.line_start(line_index + 1)?;
+
+            Ok(line_start..next_line_start)
+        }
+    }
+}
+
+/// A Diagnostic message.
+enum Message {
+    UnwantedGreetings {
+        greetings: Vec<(files::FileId, Range<usize>)>,
+    },
+    OverTheTopExclamations {
+        exclamations: Vec<(files::FileId, Range<usize>)>,
+    },
+}
+
+impl Message {
+    fn to_diagnostic(&self) -> Diagnostic<files::FileId> {
+        match self {
+            Message::UnwantedGreetings { greetings } => Diagnostic::error()
+                .with_message("greetings are not allowed")
+                .with_labels(
+                    greetings
+                        .iter()
+                        .map(|(file_id, range)| {
+                            Label::primary(*file_id, range.clone()).with_message("a greeting")
+                        })
+                        .collect(),
+                )
+                .with_notes(vec![
+                    "found greetings!".to_owned(),
+                    "pleas no greetings :(".to_owned(),
+                ]),
+            Message::OverTheTopExclamations { exclamations } => Diagnostic::error()
+                .with_message("over-the-top exclamations")
+                .with_labels(
+                    exclamations
+                        .iter()
+                        .map(|(file_id, range)| {
+                            Label::primary(*file_id, range.clone()).with_message("an exclamation")
+                        })
+                        .collect(),
+                )
+                .with_notes(vec!["ridiculous!".to_owned()]),
+        }
+    }
+}
diff --git a/examples/peg_calculator.rs b/examples/peg_calculator.rs
new file mode 100644 (file)
index 0000000..882ce6d
--- /dev/null
@@ -0,0 +1,68 @@
+//! An example of using `peg` with `codespan_reporting`.
+//!
+//! To run this example, execute the following command from the top level of
+//! this repository:
+//!
+//! ```sh
+//! cargo run --example peg_calculator
+//! ```
+
+use codespan_reporting::diagnostic::{Diagnostic, Label};
+use codespan_reporting::files::SimpleFile;
+use codespan_reporting::term;
+use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
+use rustyline::error::ReadlineError;
+use rustyline::Editor;
+
+peg::parser! {
+    grammar arithmetic() for str {
+        rule number() -> i64
+            = n:$(['0'..='9']+) { n.parse().unwrap() }
+
+        pub rule calculate() -> i64 = precedence!{
+            x:(@) "+" y:@ { x + y }
+            x:(@) "-" y:@ { x - y }
+                  "-" v:@ { - v }
+            --
+            x:(@) "*" y:@ { x * y }
+            x:(@) "/" y:@ { x / y }
+            --
+            x:@   "^" y:(@) { i64::pow(x, y as u32) }
+            v:@   "!"       { (1..v+1).product() }
+            --
+            "(" v:calculate() ")" { v }
+            n:number() { n }
+        }
+    }
+}
+
+fn main() -> anyhow::Result<()> {
+    let writer = StandardStream::stderr(ColorChoice::Always);
+    let config = codespan_reporting::term::Config::default();
+    let mut editor = Editor::<()>::new();
+
+    loop {
+        let line = match editor.readline("> ") {
+            Ok(line) => line,
+            Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => return Ok(()),
+            Err(error) => return Err(error.into()),
+        };
+
+        match arithmetic::calculate(&line) {
+            Ok(number) => println!("{}", number),
+            Err(error) => {
+                let file = SimpleFile::new("<repl>", line);
+
+                let start = error.location.offset;
+                let diagnostic = Diagnostic::error()
+                    .with_message("parse error")
+                    .with_labels(vec![
+                        Label::primary((), start..start).with_message("parse error")
+                    ])
+                    .with_notes(vec![format!("expected: {}", error.expected)]);
+
+                term::emit(&mut writer.lock(), &config, &file, &diagnostic)?;
+            }
+        }
+    }
+}
diff --git a/examples/readme_preview.rs b/examples/readme_preview.rs
new file mode 100644 (file)
index 0000000..2cf96f6
--- /dev/null
@@ -0,0 +1,356 @@
+//! Renders the preview SVG for the README.
+//!
+//! To update the preview, execute the following command from the top level of
+//! the repository:
+//!
+//! ```sh
+//! cargo run --example readme_preview svg > codespan-reporting/assets/readme_preview.svg
+//! ```
+
+use codespan_reporting::diagnostic::{Diagnostic, Label};
+use codespan_reporting::files::SimpleFile;
+use codespan_reporting::term::termcolor::{Color, ColorSpec, StandardStream, WriteColor};
+use codespan_reporting::term::{self, ColorArg};
+use std::io::{self, Write};
+use structopt::StructOpt;
+
+#[derive(Debug, StructOpt)]
+#[structopt(name = "emit")]
+pub enum Opts {
+    /// Render SVG output
+    Svg,
+    /// Render Stderr output
+    Stderr {
+        /// Configure coloring of output
+        #[structopt(
+            long = "color",
+            parse(try_from_str),
+            default_value = "auto",
+            possible_values = ColorArg::VARIANTS,
+            case_insensitive = true
+        )]
+        color: ColorArg,
+    },
+}
+
+fn main() -> anyhow::Result<()> {
+    let file = SimpleFile::new(
+        "FizzBuzz.fun",
+        unindent::unindent(
+            r#"
+                module FizzBuzz where
+
+                fizz₁ : Nat → String
+                fizz₁ num = case (mod num 5) (mod num 3) of
+                    0 0 => "FizzBuzz"
+                    0 _ => "Fizz"
+                    _ 0 => "Buzz"
+                    _ _ => num
+
+                fizz₂ : Nat → String
+                fizz₂ num =
+                    case (mod num 5) (mod num 3) of
+                        0 0 => "FizzBuzz"
+                        0 _ => "Fizz"
+                        _ 0 => "Buzz"
+                        _ _ => num
+            "#,
+        ),
+    );
+
+    let diagnostics = [Diagnostic::error()
+        .with_message("`case` clauses have incompatible types")
+        .with_code("E0308")
+        .with_labels(vec![
+            Label::primary((), 328..331).with_message("expected `String`, found `Nat`"),
+            Label::secondary((), 211..331).with_message("`case` clauses have incompatible types"),
+            Label::secondary((), 258..268).with_message("this is found to be of type `String`"),
+            Label::secondary((), 284..290).with_message("this is found to be of type `String`"),
+            Label::secondary((), 306..312).with_message("this is found to be of type `String`"),
+            Label::secondary((), 186..192).with_message("expected type `String` found here"),
+        ])
+        .with_notes(vec![unindent::unindent(
+            "
+                expected type `String`
+                   found type `Nat`
+            ",
+        )])];
+
+    // let mut files = SimpleFiles::new();
+    match Opts::from_args() {
+        Opts::Svg => {
+            let mut buffer = Vec::new();
+            let mut writer = HtmlEscapeWriter::new(SvgWriter::new(&mut buffer));
+            let config = codespan_reporting::term::Config {
+                styles: codespan_reporting::term::Styles::with_blue(Color::Blue),
+                ..codespan_reporting::term::Config::default()
+            };
+
+            for diagnostic in &diagnostics {
+                term::emit(&mut writer, &config, &file, &diagnostic)?;
+            }
+
+            let num_lines = buffer.iter().filter(|byte| **byte == b'\n').count() + 1;
+
+            let padding = 10;
+            let font_size = 12;
+            let line_spacing = 3;
+            let width = 882;
+            let height = padding + num_lines * (font_size + line_spacing) + padding;
+
+            let stdout = std::io::stdout();
+            let writer = &mut stdout.lock();
+
+            write!(
+                writer,
+                r#"<svg viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    /* https://github.com/aaron-williamson/base16-alacritty/blob/master/colors/base16-tomorrow-night-256.yml */
+    pre {{
+      background: #1d1f21;
+      margin: 0;
+      padding: {padding}px;
+      border-radius: 6px;
+      color: #ffffff;
+      font: {font_size}px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+    }}
+
+    pre .bold {{ font-weight: bold; }}
+
+    pre .fg.black   {{ color: #1d1f21; }}
+    pre .fg.red     {{ color: #cc6666; }}
+    pre .fg.green   {{ color: #b5bd68; }}
+    pre .fg.yellow  {{ color: #f0c674; }}
+    pre .fg.blue    {{ color: #81a2be; }}
+    pre .fg.magenta {{ color: #b294bb; }}
+    pre .fg.cyan    {{ color: #8abeb7; }}
+    pre .fg.white   {{ color: #c5c8c6; }}
+
+    pre .fg.black.bright    {{ color: #969896; }}
+    pre .fg.red.bright      {{ color: #cc6666; }}
+    pre .fg.green.bright    {{ color: #b5bd68; }}
+    pre .fg.yellow.bright   {{ color: #f0c674; }}
+    pre .fg.blue.bright     {{ color: #81a2be; }}
+    pre .fg.magenta.bright  {{ color: #b294bb; }}
+    pre .fg.cyan.bright     {{ color: #8abeb7; }}
+    pre .fg.white.bright    {{ color: #ffffff; }}
+
+    pre .bg.black   {{ background-color: #1d1f21; }}
+    pre .bg.red     {{ background-color: #cc6666; }}
+    pre .bg.green   {{ background-color: #b5bd68; }}
+    pre .bg.yellow  {{ background-color: #f0c674; }}
+    pre .bg.blue    {{ background-color: #81a2be; }}
+    pre .bg.magenta {{ background-color: #b294bb; }}
+    pre .bg.cyan    {{ background-color: #8abeb7; }}
+    pre .bg.white   {{ background-color: #c5c8c6; }}
+
+    pre .bg.black.bright    {{ background-color: #969896; }}
+    pre .bg.red.bright      {{ background-color: #cc6666; }}
+    pre .bg.green.bright    {{ background-color: #b5bd68; }}
+    pre .bg.yellow.bright   {{ background-color: #f0c674; }}
+    pre .bg.blue.bright     {{ background-color: #81a2be; }}
+    pre .bg.magenta.bright  {{ background-color: #b294bb; }}
+    pre .bg.cyan.bright     {{ background-color: #8abeb7; }}
+    pre .bg.white.bright    {{ background-color: #ffffff; }}
+  </style>
+
+  <foreignObject x="0" y="0" width="{width}" height="{height}">
+    <div xmlns="http://www.w3.org/1999/xhtml">
+      <pre>"#,
+                padding = padding,
+                font_size = font_size,
+                width = width,
+                height = height,
+            )?;
+
+            writer.write_all(&buffer)?;
+
+            write!(
+                writer,
+                "</pre>
+    </div>
+  </foreignObject>
+</svg>
+"
+            )?;
+        }
+        Opts::Stderr { color } => {
+            let writer = StandardStream::stderr(color.into());
+            let config = codespan_reporting::term::Config::default();
+            for diagnostic in &diagnostics {
+                term::emit(&mut writer.lock(), &config, &file, &diagnostic)?;
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Rudimentary HTML escaper which performs the following conversions:
+///
+/// - `<` ⇒ `&lt;`
+/// - `>` ⇒ `&gt;`
+/// - `&` ⇒ `&amp;`
+pub struct HtmlEscapeWriter<W> {
+    upstream: W,
+}
+
+impl<W> HtmlEscapeWriter<W> {
+    pub fn new(upstream: W) -> HtmlEscapeWriter<W> {
+        HtmlEscapeWriter { upstream }
+    }
+}
+
+impl<W: Write> Write for HtmlEscapeWriter<W> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        let mut last_term = 0usize;
+        for (i, byte) in buf.iter().enumerate() {
+            let escape = match byte {
+                b'<' => &b"&lt;"[..],
+                b'>' => &b"&gt;"[..],
+                b'&' => &b"&amp;"[..],
+                _ => continue,
+            };
+            self.upstream.write_all(&buf[last_term..i])?;
+            last_term = i + 1;
+            self.upstream.write_all(escape)?;
+        }
+        self.upstream.write_all(&buf[last_term..])?;
+        Ok(buf.len())
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.upstream.flush()
+    }
+}
+
+impl<W: WriteColor> WriteColor for HtmlEscapeWriter<W> {
+    fn supports_color(&self) -> bool {
+        self.upstream.supports_color()
+    }
+
+    fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
+        self.upstream.set_color(spec)
+    }
+
+    fn reset(&mut self) -> io::Result<()> {
+        self.upstream.reset()
+    }
+}
+
+pub struct SvgWriter<W> {
+    upstream: W,
+    color: ColorSpec,
+}
+
+impl<W> SvgWriter<W> {
+    pub fn new(upstream: W) -> SvgWriter<W> {
+        SvgWriter {
+            upstream,
+            color: ColorSpec::new(),
+        }
+    }
+}
+
+impl<W: Write> Write for SvgWriter<W> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.upstream.write(buf)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.upstream.flush()
+    }
+}
+
+impl<W: Write> WriteColor for SvgWriter<W> {
+    fn supports_color(&self) -> bool {
+        true
+    }
+
+    fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
+        #![allow(unused_assignments)]
+
+        if self.color == *spec {
+            return Ok(());
+        } else {
+            if !self.color.is_none() {
+                write!(self, "</span>")?;
+            }
+            self.color = spec.clone();
+        }
+
+        if spec.is_none() {
+            write!(self, "</span>")?;
+            return Ok(());
+        } else {
+            write!(self, "<span class=\"")?;
+        }
+
+        let mut first = true;
+
+        fn write_first<W: Write>(first: bool, writer: &mut SvgWriter<W>) -> io::Result<bool> {
+            if !first {
+                write!(writer, " ")?;
+            }
+
+            Ok(false)
+        };
+
+        fn write_color<W: Write>(color: &Color, writer: &mut SvgWriter<W>) -> io::Result<()> {
+            match color {
+                Color::Black => write!(writer, "black"),
+                Color::Blue => write!(writer, "blue"),
+                Color::Green => write!(writer, "green"),
+                Color::Red => write!(writer, "red"),
+                Color::Cyan => write!(writer, "cyan"),
+                Color::Magenta => write!(writer, "magenta"),
+                Color::Yellow => write!(writer, "yellow"),
+                Color::White => write!(writer, "white"),
+                // TODO: other colors
+                _ => Ok(()),
+            }
+        };
+
+        if let Some(fg) = spec.fg() {
+            first = write_first(first, self)?;
+            write!(self, "fg ")?;
+            write_color(fg, self)?;
+        }
+
+        if let Some(bg) = spec.bg() {
+            first = write_first(first, self)?;
+            write!(self, "bg ")?;
+            write_color(bg, self)?;
+        }
+
+        if spec.bold() {
+            first = write_first(first, self)?;
+            write!(self, "bold")?;
+        }
+
+        if spec.underline() {
+            first = write_first(first, self)?;
+            write!(self, "underline")?;
+        }
+
+        if spec.intense() {
+            first = write_first(first, self)?;
+            write!(self, "bright")?;
+        }
+
+        write!(self, "\">")?;
+
+        Ok(())
+    }
+
+    fn reset(&mut self) -> io::Result<()> {
+        let color = self.color.clone();
+
+        if color != ColorSpec::new() {
+            write!(self, "</span>")?;
+            self.color = ColorSpec::new();
+        }
+
+        Ok(())
+    }
+}
diff --git a/examples/reusable_diagnostic.rs b/examples/reusable_diagnostic.rs
new file mode 100644 (file)
index 0000000..d05dee8
--- /dev/null
@@ -0,0 +1,105 @@
+use codespan_reporting::diagnostic::{Diagnostic, Label};
+use codespan_reporting::files::SimpleFile;
+use codespan_reporting::term::termcolor::StandardStream;
+use codespan_reporting::term::{self, ColorArg};
+use std::ops::Range;
+use structopt::StructOpt;
+
+#[derive(Debug, StructOpt)]
+#[structopt(name = "emit")]
+pub struct Opts {
+    #[structopt(long = "color",
+        parse(try_from_str),
+        default_value = "auto",
+        possible_values = ColorArg::VARIANTS,
+        case_insensitive = true
+    )]
+    color: ColorArg,
+}
+
+fn main() -> anyhow::Result<()> {
+    let file = SimpleFile::new(
+        "main.rs",
+        unindent::unindent(
+            r#"
+                fn main() {
+                    let foo: i32 = "hello, world";
+                    foo += 1;
+                }
+            "#,
+        ),
+    );
+
+    let errors = [
+        Error::MismatchType(
+            Item::new(20..23, "i32"),
+            Item::new(31..45, "\"hello, world\""),
+        ),
+        Error::MutatingImmutable(Item::new(20..23, "foo"), Item::new(51..59, "foo += 1")),
+    ];
+
+    let opts = Opts::from_args();
+    let writer = StandardStream::stderr(opts.color.into());
+    let config = codespan_reporting::term::Config::default();
+    for diagnostic in errors.iter().map(Error::report) {
+        term::emit(&mut writer.lock(), &config, &file, &diagnostic)?;
+    }
+
+    Ok(())
+}
+
+/// An error enum that represent all possible errors within your program
+enum Error {
+    MismatchType(Item, Item),
+    MutatingImmutable(Item, Item),
+}
+
+impl Error {
+    fn report(&self) -> Diagnostic<()> {
+        match self {
+            Error::MismatchType(left, right) => Diagnostic::error()
+                .with_code("E0308")
+                .with_message("mismatch types")
+                .with_labels(vec![
+                    Label::primary((), right.range.clone()).with_message(format!(
+                        "Expected `{}`, found: `{}`",
+                        left.content, right.content,
+                    )),
+                    Label::secondary((), left.range.clone()).with_message("expected due to this"),
+                ]),
+            Error::MutatingImmutable(original, mutating) => Diagnostic::error()
+                .with_code("E0384")
+                .with_message(format!(
+                    "cannot mutate immutable variable `{}`",
+                    original.content,
+                ))
+                .with_labels(vec![
+                    Label::secondary((), original.range.clone()).with_message(unindent::unindent(
+                        &format!(
+                            r#"
+                                first assignment to `{0}`
+                                help: make this binding mutable: `mut {0}`
+                            "#,
+                            original.content,
+                        ),
+                    )),
+                    Label::primary((), mutating.range.clone())
+                        .with_message("cannot assign twice to immutable variable"),
+                ]),
+        }
+    }
+}
+
+/// An item in the source code to be used in the `Error` enum.
+/// In a more complex program it could also contain a `files::FileId` to handle errors that occur inside multiple files.
+struct Item {
+    range: Range<usize>,
+    content: String,
+}
+
+impl Item {
+    fn new(range: Range<usize>, content: impl Into<String>) -> Item {
+        let content = content.into();
+        Item { range, content }
+    }
+}
diff --git a/examples/term.rs b/examples/term.rs
new file mode 100644 (file)
index 0000000..19bf850
--- /dev/null
@@ -0,0 +1,175 @@
+//! To run this example, execute the following command from the top level of
+//! this repository:
+//!
+//! ```sh
+//! cargo run --example term
+//! ```
+
+use codespan_reporting::diagnostic::{Diagnostic, Label};
+use codespan_reporting::files::SimpleFiles;
+use codespan_reporting::term::termcolor::StandardStream;
+use codespan_reporting::term::{self, ColorArg};
+use structopt::StructOpt;
+
+#[derive(Debug, StructOpt)]
+#[structopt(name = "emit")]
+pub struct Opts {
+    /// Configure coloring of output
+    #[structopt(
+        long = "color",
+        parse(try_from_str),
+        default_value = "auto",
+        possible_values = ColorArg::VARIANTS,
+        case_insensitive = true
+    )]
+    pub color: ColorArg,
+}
+
+fn main() -> anyhow::Result<()> {
+    let opts = Opts::from_args();
+    let mut files = SimpleFiles::new();
+
+    let file_id1 = files.add(
+        "Data/Nat.fun",
+        unindent::unindent(
+            "
+                module Data.Nat where
+
+                data Nat : Type where
+                    zero : Nat
+                    succ : Nat → Nat
+
+                {-# BUILTIN NATRAL Nat #-}
+
+                infixl 6 _+_ _-_
+
+                _+_ : Nat → Nat → Nat
+                zero    + n₂ = n₂
+                succ n₁ + n₂ = succ (n₁ + n₂)
+
+                _-_ : Nat → Nat → Nat
+                n₁      - zero    = n₁
+                zero    - succ n₂ = zero
+                succ n₁ - succ n₂ = n₁ - n₂
+            ",
+        ),
+    );
+
+    let file_id2 = files.add(
+        "Test.fun",
+        unindent::unindent(
+            r#"
+                module Test where
+
+                _ : Nat
+                _ = 123 + "hello"
+            "#,
+        ),
+    );
+
+    let file_id3 = files.add(
+        "FizzBuzz.fun",
+        unindent::unindent(
+            r#"
+                module FizzBuzz where
+
+                fizz₁ : Nat → String
+                fizz₁ num = case (mod num 5) (mod num 3) of
+                    0 0 => "FizzBuzz"
+                    0 _ => "Fizz"
+                    _ 0 => "Buzz"
+                    _ _ => num
+
+                fizz₂ : Nat → String
+                fizz₂ num =
+                    case (mod num 5) (mod num 3) of
+                        0 0 => "FizzBuzz"
+                        0 _ => "Fizz"
+                        _ 0 => "Buzz"
+                        _ _ => num
+            "#,
+        ),
+    );
+
+    let diagnostics = [
+        // Unknown builtin error
+        Diagnostic::error()
+            .with_message("unknown builtin: `NATRAL`")
+            .with_labels(vec![
+                Label::primary(file_id1, 96..102).with_message("unknown builtin")
+            ])
+            .with_notes(vec![
+                "there is a builtin with a similar name: `NATURAL`".to_owned()
+            ]),
+        // Unused parameter warning
+        Diagnostic::warning()
+            .with_message("unused parameter pattern: `n₂`")
+            .with_labels(vec![
+                Label::primary(file_id1, 285..289).with_message("unused parameter")
+            ])
+            .with_notes(vec!["consider using a wildcard pattern: `_`".to_owned()]),
+        // Unexpected type error
+        Diagnostic::error()
+            .with_message("unexpected type in application of `_+_`")
+            .with_code("E0001")
+            .with_labels(vec![
+                Label::primary(file_id2, 37..44).with_message("expected `Nat`, found `String`"),
+                Label::secondary(file_id1, 130..155)
+                    .with_message("based on the definition of `_+_`"),
+            ])
+            .with_notes(vec![unindent::unindent(
+                "
+                    expected type `Nat`
+                       found type `String`
+                ",
+            )]),
+        // Incompatible match clause error
+        Diagnostic::error()
+            .with_message("`case` clauses have incompatible types")
+            .with_code("E0308")
+            .with_labels(vec![
+                Label::primary(file_id3, 163..166).with_message("expected `String`, found `Nat`"),
+                Label::secondary(file_id3, 62..166)
+                    .with_message("`case` clauses have incompatible types"),
+                Label::secondary(file_id3, 41..47)
+                    .with_message("expected type `String` found here"),
+            ])
+            .with_notes(vec![unindent::unindent(
+                "
+                    expected type `String`
+                       found type `Nat`
+                ",
+            )]),
+        // Incompatible match clause error
+        Diagnostic::error()
+            .with_message("`case` clauses have incompatible types")
+            .with_code("E0308")
+            .with_labels(vec![
+                Label::primary(file_id3, 328..331).with_message("expected `String`, found `Nat`"),
+                Label::secondary(file_id3, 211..331)
+                    .with_message("`case` clauses have incompatible types"),
+                Label::secondary(file_id3, 258..268)
+                    .with_message("this is found to be of type `String`"),
+                Label::secondary(file_id3, 284..290)
+                    .with_message("this is found to be of type `String`"),
+                Label::secondary(file_id3, 306..312)
+                    .with_message("this is found to be of type `String`"),
+                Label::secondary(file_id3, 186..192)
+                    .with_message("expected type `String` found here"),
+            ])
+            .with_notes(vec![unindent::unindent(
+                "
+                    expected type `String`
+                       found type `Nat`
+                ",
+            )]),
+    ];
+
+    let writer = StandardStream::stderr(opts.color.into());
+    let config = codespan_reporting::term::Config::default();
+    for diagnostic in &diagnostics {
+        term::emit(&mut writer.lock(), &config, &files, &diagnostic)?;
+    }
+
+    Ok(())
+}
diff --git a/src/diagnostic.rs b/src/diagnostic.rs
new file mode 100644 (file)
index 0000000..c1f98bd
--- /dev/null
@@ -0,0 +1,209 @@
+//! Diagnostic data structures.
+
+#[cfg(feature = "serialization")]
+use serde::{Deserialize, Serialize};
+use std::ops::Range;
+
+/// A severity level for diagnostic messages.
+///
+/// These are ordered in the following way:
+///
+/// ```rust
+/// use codespan_reporting::diagnostic::Severity;
+///
+/// assert!(Severity::Bug > Severity::Error);
+/// assert!(Severity::Error > Severity::Warning);
+/// assert!(Severity::Warning > Severity::Note);
+/// assert!(Severity::Note > Severity::Help);
+/// ```
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
+pub enum Severity {
+    /// An unexpected bug.
+    Bug,
+    /// An error.
+    Error,
+    /// A warning.
+    Warning,
+    /// A note.
+    Note,
+    /// A help message.
+    Help,
+}
+
+impl Severity {
+    /// We want bugs to be the maximum severity, errors next, etc...
+    fn to_cmp_int(self) -> u8 {
+        match self {
+            Severity::Bug => 5,
+            Severity::Error => 4,
+            Severity::Warning => 3,
+            Severity::Note => 2,
+            Severity::Help => 1,
+        }
+    }
+}
+
+impl PartialOrd for Severity {
+    fn partial_cmp(&self, other: &Severity) -> Option<std::cmp::Ordering> {
+        u8::partial_cmp(&self.to_cmp_int(), &other.to_cmp_int())
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd)]
+#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
+pub enum LabelStyle {
+    /// Labels that describe the primary cause of a diagnostic.
+    Primary,
+    /// Labels that provide additional context for a diagnostic.
+    Secondary,
+}
+
+/// A label describing an underlined region of code associated with a diagnostic.
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
+pub struct Label<FileId> {
+    /// The style of the label.
+    pub style: LabelStyle,
+    /// The file that we are labelling.
+    pub file_id: FileId,
+    /// The range in bytes we are going to include in the final snippet.
+    pub range: Range<usize>,
+    /// An optional message to provide some additional information for the
+    /// underlined code. These should not include line breaks.
+    pub message: String,
+}
+
+impl<FileId> Label<FileId> {
+    /// Create a new label.
+    pub fn new(
+        style: LabelStyle,
+        file_id: FileId,
+        range: impl Into<Range<usize>>,
+    ) -> Label<FileId> {
+        Label {
+            style,
+            file_id,
+            range: range.into(),
+            message: String::new(),
+        }
+    }
+
+    /// Create a new label with a style of [`LabelStyle::Primary`].
+    ///
+    /// [`LabelStyle::Primary`]: LabelStyle::Primary
+    pub fn primary(file_id: FileId, range: impl Into<Range<usize>>) -> Label<FileId> {
+        Label::new(LabelStyle::Primary, file_id, range)
+    }
+
+    /// Create a new label with a style of [`LabelStyle::Secondary`].
+    ///
+    /// [`LabelStyle::Secondary`]: LabelStyle::Secondary
+    pub fn secondary(file_id: FileId, range: impl Into<Range<usize>>) -> Label<FileId> {
+        Label::new(LabelStyle::Secondary, file_id, range)
+    }
+
+    /// Add a message to the diagnostic.
+    pub fn with_message(mut self, message: impl Into<String>) -> Label<FileId> {
+        self.message = message.into();
+        self
+    }
+}
+
+/// Represents a diagnostic message that can provide information like errors and
+/// warnings to the user.
+///
+/// The position of a Diagnostic is considered to be the position of the [`Label`] that has the earliest starting position and has the highest style which appears in all the labels of the diagnostic.
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
+pub struct Diagnostic<FileId> {
+    /// The overall severity of the diagnostic
+    pub severity: Severity,
+    /// An optional code that identifies this diagnostic.
+    pub code: Option<String>,
+    /// The main message associated with this diagnostic.
+    ///
+    /// These should not include line breaks, and in order support the 'short'
+    /// diagnostic display mod, the message should be specific enough to make
+    /// sense on its own, without additional context provided by labels and notes.
+    pub message: String,
+    /// Source labels that describe the cause of the diagnostic.
+    /// The order of the labels inside the vector does not have any meaning.
+    /// The labels are always arranged in the order they appear in the source code.
+    pub labels: Vec<Label<FileId>>,
+    /// Notes that are associated with the primary cause of the diagnostic.
+    /// These can include line breaks for improved formatting.
+    pub notes: Vec<String>,
+}
+
+impl<FileId> Diagnostic<FileId> {
+    /// Create a new diagnostic.
+    pub fn new(severity: Severity) -> Diagnostic<FileId> {
+        Diagnostic {
+            severity,
+            code: None,
+            message: String::new(),
+            labels: Vec::new(),
+            notes: Vec::new(),
+        }
+    }
+
+    /// Create a new diagnostic with a severity of [`Severity::Bug`].
+    ///
+    /// [`Severity::Bug`]: Severity::Bug
+    pub fn bug() -> Diagnostic<FileId> {
+        Diagnostic::new(Severity::Bug)
+    }
+
+    /// Create a new diagnostic with a severity of [`Severity::Error`].
+    ///
+    /// [`Severity::Error`]: Severity::Error
+    pub fn error() -> Diagnostic<FileId> {
+        Diagnostic::new(Severity::Error)
+    }
+
+    /// Create a new diagnostic with a severity of [`Severity::Warning`].
+    ///
+    /// [`Severity::Warning`]: Severity::Warning
+    pub fn warning() -> Diagnostic<FileId> {
+        Diagnostic::new(Severity::Warning)
+    }
+
+    /// Create a new diagnostic with a severity of [`Severity::Note`].
+    ///
+    /// [`Severity::Note`]: Severity::Note
+    pub fn note() -> Diagnostic<FileId> {
+        Diagnostic::new(Severity::Note)
+    }
+
+    /// Create a new diagnostic with a severity of [`Severity::Help`].
+    ///
+    /// [`Severity::Help`]: Severity::Help
+    pub fn help() -> Diagnostic<FileId> {
+        Diagnostic::new(Severity::Help)
+    }
+
+    /// Set the error code of the diagnostic.
+    pub fn with_code(mut self, code: impl Into<String>) -> Diagnostic<FileId> {
+        self.code = Some(code.into());
+        self
+    }
+
+    /// Set the message of the diagnostic.
+    pub fn with_message(mut self, message: impl Into<String>) -> Diagnostic<FileId> {
+        self.message = message.into();
+        self
+    }
+
+    /// Add some labels to the diagnostic.
+    pub fn with_labels(mut self, mut labels: Vec<Label<FileId>>) -> Diagnostic<FileId> {
+        self.labels.append(&mut labels);
+        self
+    }
+
+    /// Add some notes to the diagnostic.
+    pub fn with_notes(mut self, mut notes: Vec<String>) -> Diagnostic<FileId> {
+        self.notes.append(&mut notes);
+        self
+    }
+}
diff --git a/src/files.rs b/src/files.rs
new file mode 100644 (file)
index 0000000..b25cd79
--- /dev/null
@@ -0,0 +1,443 @@
+//! Source file support for diagnostic reporting.
+//!
+//! The main trait defined in this module is the [`Files`] trait, which provides
+//! provides the minimum amount of functionality required for printing [`Diagnostics`]
+//! with the [`term::emit`] function.
+//!
+//! Simple implementations of this trait are implemented:
+//!
+//! - [`SimpleFile`]: For single-file use-cases
+//! - [`SimpleFiles`]: For multi-file use-cases
+//!
+//! These data structures provide a pretty minimal API, however,
+//! so end-users are encouraged to create their own implementations for their
+//! own specific use-cases, such as an implementation that accesses the file
+//! system directly (and caches the line start locations), or an implementation
+//! using an incremental compilation library like [`salsa`].
+//!
+//! [`term::emit`]: crate::term::emit
+//! [`Diagnostics`]: crate::diagnostic::Diagnostic
+//! [`Files`]: Files
+//! [`SimpleFile`]: SimpleFile
+//! [`SimpleFiles`]: SimpleFiles
+//!
+//! [`salsa`]: https://crates.io/crates/salsa
+
+use std::ops::Range;
+
+/// An enum representing an error that happened while looking up a file or a piece of content in that file.
+#[derive(Debug)]
+#[non_exhaustive]
+pub enum Error {
+    /// A required file is not in the file database.
+    FileMissing,
+    /// The file is present, but does not contain the specified byte index.
+    IndexTooLarge { given: usize, max: usize },
+    /// The file is present, but does not contain the specified line index.
+    LineTooLarge { given: usize, max: usize },
+    /// The file is present and contains the specified line index, but the line does not contain the specified column index.
+    ColumnTooLarge { given: usize, max: usize },
+    /// The given index is contained in the file, but is not a boundary of a UTF-8 code point.
+    InvalidCharBoundary { given: usize },
+    /// There was a error while doing IO.
+    Io(std::io::Error),
+}
+
+impl From<std::io::Error> for Error {
+    fn from(err: std::io::Error) -> Error {
+        Error::Io(err)
+    }
+}
+
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Error::FileMissing => write!(f, "file missing"),
+            Error::IndexTooLarge { given, max } => {
+                write!(f, "invalid index {}, maximum index is {}", given, max)
+            }
+            Error::LineTooLarge { given, max } => {
+                write!(f, "invalid line {}, maximum line is {}", given, max)
+            }
+            Error::ColumnTooLarge { given, max } => {
+                write!(f, "invalid column {}, maximum column {}", given, max)
+            }
+            Error::InvalidCharBoundary { .. } => write!(f, "index is not a code point boundary"),
+            Error::Io(err) => write!(f, "{}", err),
+        }
+    }
+}
+
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match &self {
+            Error::Io(err) => Some(err),
+            _ => None,
+        }
+    }
+}
+
+/// A minimal interface for accessing source files when rendering diagnostics.
+///
+/// A lifetime parameter `'a` is provided to allow any of the returned values to returned by reference.
+/// This is to workaround the lack of higher kinded lifetime parameters.
+/// This can be ignored if this is not needed, however.
+pub trait Files<'a> {
+    /// A unique identifier for files in the file provider. This will be used
+    /// for rendering `diagnostic::Label`s in the corresponding source files.
+    type FileId: 'a + Copy + PartialEq;
+    /// The user-facing name of a file, to be displayed in diagnostics.
+    type Name: 'a + std::fmt::Display;
+    /// The source code of a file.
+    type Source: 'a + AsRef<str>;
+
+    /// The user-facing name of a file.
+    fn name(&'a self, id: Self::FileId) -> Result<Self::Name, Error>;
+
+    /// The source code of a file.
+    fn source(&'a self, id: Self::FileId) -> Result<Self::Source, Error>;
+
+    /// The index of the line at the given byte index.
+    /// If the byte index is past the end of the file, returns the maximum line index in the file.
+    /// This means that this function only fails if the file is not present.
+    ///
+    /// # Note for trait implementors
+    ///
+    /// This can be implemented efficiently by performing a binary search over
+    /// a list of line starts that was computed by calling the [`line_starts`]
+    /// function that is exported from the [`files`] module. It might be useful
+    /// to pre-compute and cache these line starts.
+    ///
+    /// [`line_starts`]: crate::files::line_starts
+    /// [`files`]: crate::files
+    fn line_index(&'a self, id: Self::FileId, byte_index: usize) -> Result<usize, Error>;
+
+    /// The user-facing line number at the given line index.
+    /// It is not necessarily checked that the specified line index
+    /// is actually in the file.
+    ///
+    /// # Note for trait implementors
+    ///
+    /// This is usually 1-indexed from the beginning of the file, but
+    /// can be useful for implementing something like the
+    /// [C preprocessor's `#line` macro][line-macro].
+    ///
+    /// [line-macro]: https://en.cppreference.com/w/c/preprocessor/line
+    #[allow(unused_variables)]
+    fn line_number(&'a self, id: Self::FileId, line_index: usize) -> Result<usize, Error> {
+        Ok(line_index + 1)
+    }
+
+    /// The user-facing column number at the given line index and byte index.
+    ///
+    /// # Note for trait implementors
+    ///
+    /// This is usually 1-indexed from the the start of the line.
+    /// A default implementation is provided, based on the [`column_index`]
+    /// function that is exported from the [`files`] module.
+    ///
+    /// [`files`]: crate::files
+    /// [`column_index`]: crate::files::column_index
+    fn column_number(
+        &'a self,
+        id: Self::FileId,
+        line_index: usize,
+        byte_index: usize,
+    ) -> Result<usize, Error> {
+        let source = self.source(id)?;
+        let line_range = self.line_range(id, line_index)?;
+        let column_index = column_index(source.as_ref(), line_range, byte_index);
+
+        Ok(column_index + 1)
+    }
+
+    /// Convenience method for returning line and column number at the given
+    /// byte index in the file.
+    fn location(&'a self, id: Self::FileId, byte_index: usize) -> Result<Location, Error> {
+        let line_index = self.line_index(id, byte_index)?;
+
+        Ok(Location {
+            line_number: self.line_number(id, line_index)?,
+            column_number: self.column_number(id, line_index, byte_index)?,
+        })
+    }
+
+    /// The byte range of line in the source of the file.
+    fn line_range(&'a self, id: Self::FileId, line_index: usize) -> Result<Range<usize>, Error>;
+}
+
+/// A user-facing location in a source file.
+///
+/// Returned by [`Files::location`].
+///
+/// [`Files::location`]: Files::location
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct Location {
+    /// The user-facing line number.
+    pub line_number: usize,
+    /// The user-facing column number.
+    pub column_number: usize,
+}
+
+/// The column index at the given byte index in the source file.
+/// This is the number of characters to the given byte index.
+///
+/// If the byte index is smaller than the start of the line, then `0` is returned.
+/// If the byte index is past the end of the line, the column index of the last
+/// character `+ 1` is returned.
+///
+/// # Example
+///
+/// ```rust
+/// use codespan_reporting::files;
+///
+/// let source = "\n\n🗻∈🌏\n\n";
+///
+/// assert_eq!(files::column_index(source, 0..1, 0), 0);
+/// assert_eq!(files::column_index(source, 2..13, 0), 0);
+/// assert_eq!(files::column_index(source, 2..13, 2 + 0), 0);
+/// assert_eq!(files::column_index(source, 2..13, 2 + 1), 0);
+/// assert_eq!(files::column_index(source, 2..13, 2 + 4), 1);
+/// assert_eq!(files::column_index(source, 2..13, 2 + 8), 2);
+/// assert_eq!(files::column_index(source, 2..13, 2 + 10), 2);
+/// assert_eq!(files::column_index(source, 2..13, 2 + 11), 3);
+/// assert_eq!(files::column_index(source, 2..13, 2 + 12), 3);
+/// ```
+pub fn column_index(source: &str, line_range: Range<usize>, byte_index: usize) -> usize {
+    let end_index = std::cmp::min(byte_index, std::cmp::min(line_range.end, source.len()));
+
+    (line_range.start..end_index)
+        .filter(|byte_index| source.is_char_boundary(byte_index + 1))
+        .count()
+}
+
+/// Return the starting byte index of each line in the source string.
+///
+/// This can make it easier to implement [`Files::line_index`] by allowing
+/// implementors of [`Files`] to pre-compute the line starts, then search for
+/// the corresponding line range, as shown in the example below.
+///
+/// [`Files`]: Files
+/// [`Files::line_index`]: Files::line_index
+///
+/// # Example
+///
+/// ```rust
+/// use codespan_reporting::files;
+///
+/// let source = "foo\nbar\r\n\nbaz";
+/// let line_starts: Vec<_> = files::line_starts(source).collect();
+///
+/// assert_eq!(
+///     line_starts,
+///     [
+///         0,  // "foo\n"
+///         4,  // "bar\r\n"
+///         9,  // ""
+///         10, // "baz"
+///     ],
+/// );
+///
+/// fn line_index(line_starts: &[usize], byte_index: usize) -> Option<usize> {
+///     match line_starts.binary_search(&byte_index) {
+///         Ok(line) => Some(line),
+///         Err(next_line) => Some(next_line - 1),
+///     }
+/// }
+///
+/// assert_eq!(line_index(&line_starts, 5), Some(1));
+/// ```
+// NOTE: this is copied in `codespan::file::line_starts` and should be kept in sync.
+pub fn line_starts<'source>(source: &'source str) -> impl 'source + Iterator<Item = usize> {
+    std::iter::once(0).chain(source.match_indices('\n').map(|(i, _)| i + 1))
+}
+
+/// A file database that contains a single source file.
+///
+/// Because there is only single file in this database we use `()` as a [`FileId`].
+///
+/// This is useful for simple language tests, but it might be worth creating a
+/// custom implementation when a language scales beyond a certain size.
+///
+/// [`FileId`]: Files::FileId
+#[derive(Debug, Clone)]
+pub struct SimpleFile<Name, Source> {
+    /// The name of the file.
+    name: Name,
+    /// The source code of the file.
+    source: Source,
+    /// The starting byte indices in the source code.
+    line_starts: Vec<usize>,
+}
+
+impl<Name, Source> SimpleFile<Name, Source>
+where
+    Name: std::fmt::Display,
+    Source: AsRef<str>,
+{
+    /// Create a new source file.
+    pub fn new(name: Name, source: Source) -> SimpleFile<Name, Source> {
+        SimpleFile {
+            name,
+            line_starts: line_starts(source.as_ref()).collect(),
+            source,
+        }
+    }
+
+    /// Return the name of the file.
+    pub fn name(&self) -> &Name {
+        &self.name
+    }
+
+    /// Return the source of the file.
+    pub fn source(&self) -> &Source {
+        &self.source
+    }
+
+    /// Return the starting byte index of the line with the specified line index.
+    /// Convenience method that already generates errors if necessary.
+    fn line_start(&self, line_index: usize) -> Result<usize, Error> {
+        use std::cmp::Ordering;
+
+        match line_index.cmp(&self.line_starts.len()) {
+            Ordering::Less => Ok(self
+                .line_starts
+                .get(line_index)
+                .cloned()
+                .expect("failed despite previous check")),
+            Ordering::Equal => Ok(self.source.as_ref().len()),
+            Ordering::Greater => Err(Error::LineTooLarge {
+                given: line_index,
+                max: self.line_starts.len() - 1,
+            }),
+        }
+    }
+}
+
+impl<'a, Name, Source> Files<'a> for SimpleFile<Name, Source>
+where
+    Name: 'a + std::fmt::Display + Clone,
+    Source: 'a + AsRef<str>,
+{
+    type FileId = ();
+    type Name = Name;
+    type Source = &'a str;
+
+    fn name(&self, (): ()) -> Result<Name, Error> {
+        Ok(self.name.clone())
+    }
+
+    fn source(&self, (): ()) -> Result<&str, Error> {
+        Ok(self.source.as_ref())
+    }
+
+    fn line_index(&self, (): (), byte_index: usize) -> Result<usize, Error> {
+        Ok(self
+            .line_starts
+            .binary_search(&byte_index)
+            .unwrap_or_else(|next_line| next_line - 1))
+    }
+
+    fn line_range(&self, (): (), line_index: usize) -> Result<Range<usize>, Error> {
+        let line_start = self.line_start(line_index)?;
+        let next_line_start = self.line_start(line_index + 1)?;
+
+        Ok(line_start..next_line_start)
+    }
+}
+
+/// A file database that can store multiple source files.
+///
+/// This is useful for simple language tests, but it might be worth creating a
+/// custom implementation when a language scales beyond a certain size.
+/// It is a glorified `Vec<SimpleFile>` that implements the `Files` trait.
+#[derive(Debug, Clone)]
+pub struct SimpleFiles<Name, Source> {
+    files: Vec<SimpleFile<Name, Source>>,
+}
+
+impl<Name, Source> SimpleFiles<Name, Source>
+where
+    Name: std::fmt::Display,
+    Source: AsRef<str>,
+{
+    /// Create a new files database.
+    pub fn new() -> SimpleFiles<Name, Source> {
+        SimpleFiles { files: Vec::new() }
+    }
+
+    /// Add a file to the database, returning the handle that can be used to
+    /// refer to it again.
+    pub fn add(&mut self, name: Name, source: Source) -> usize {
+        let file_id = self.files.len();
+        self.files.push(SimpleFile::new(name, source));
+        file_id
+    }
+
+    /// Get the file corresponding to the given id.
+    pub fn get(&self, file_id: usize) -> Result<&SimpleFile<Name, Source>, Error> {
+        self.files.get(file_id).ok_or(Error::FileMissing)
+    }
+}
+
+impl<'a, Name, Source> Files<'a> for SimpleFiles<Name, Source>
+where
+    Name: 'a + std::fmt::Display + Clone,
+    Source: 'a + AsRef<str>,
+{
+    type FileId = usize;
+    type Name = Name;
+    type Source = &'a str;
+
+    fn name(&self, file_id: usize) -> Result<Name, Error> {
+        Ok(self.get(file_id)?.name().clone())
+    }
+
+    fn source(&self, file_id: usize) -> Result<&str, Error> {
+        Ok(self.get(file_id)?.source().as_ref())
+    }
+
+    fn line_index(&self, file_id: usize, byte_index: usize) -> Result<usize, Error> {
+        self.get(file_id)?.line_index((), byte_index)
+    }
+
+    fn line_range(&self, file_id: usize, line_index: usize) -> Result<Range<usize>, Error> {
+        self.get(file_id)?.line_range((), line_index)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    const TEST_SOURCE: &str = "foo\nbar\r\n\nbaz";
+
+    #[test]
+    fn line_starts() {
+        let file = SimpleFile::new("test", TEST_SOURCE);
+
+        assert_eq!(
+            file.line_starts,
+            [
+                0,  // "foo\n"
+                4,  // "bar\r\n"
+                9,  // ""
+                10, // "baz"
+            ],
+        );
+    }
+
+    #[test]
+    fn line_span_sources() {
+        let file = SimpleFile::new("test", TEST_SOURCE);
+
+        let line_sources = (0..4)
+            .map(|line| {
+                let line_range = file.line_range((), line).unwrap();
+                &file.source[line_range]
+            })
+            .collect::<Vec<_>>();
+
+        assert_eq!(line_sources, ["foo\n", "bar\r\n", "\n", "baz"]);
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..28d7f24
--- /dev/null
@@ -0,0 +1,7 @@
+//! Diagnostic reporting support for the codespan crate.
+
+#![forbid(unsafe_code)]
+
+pub mod diagnostic;
+pub mod files;
+pub mod term;
diff --git a/src/term.rs b/src/term.rs
new file mode 100644 (file)
index 0000000..59baeb0
--- /dev/null
@@ -0,0 +1,121 @@
+//! Terminal back-end for emitting diagnostics.
+
+use std::str::FromStr;
+use termcolor::{ColorChoice, WriteColor};
+
+use crate::diagnostic::Diagnostic;
+use crate::files::Files;
+
+mod config;
+mod renderer;
+mod views;
+
+pub use termcolor;
+
+pub use self::config::{Chars, Config, DisplayStyle, Styles};
+
+/// A command line argument that configures the coloring of the output.
+///
+/// This can be used with command line argument parsers like [`clap`] or [`structopt`].
+///
+/// [`clap`]: https://crates.io/crates/clap
+/// [`structopt`]: https://crates.io/crates/structopt
+///
+/// # Example
+///
+/// ```rust
+/// use codespan_reporting::term::termcolor::StandardStream;
+/// use codespan_reporting::term::ColorArg;
+/// use structopt::StructOpt;
+///
+/// #[derive(Debug, StructOpt)]
+/// #[structopt(name = "groovey-app")]
+/// pub struct Opts {
+///     /// Configure coloring of output
+///     #[structopt(
+///         long = "color",
+///         default_value = "auto",
+///         possible_values = ColorArg::VARIANTS,
+///         case_insensitive = true,
+///     )]
+///     pub color: ColorArg,
+/// }
+///
+/// let opts = Opts::from_args();
+/// let writer = StandardStream::stderr(opts.color.into());
+/// ```
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct ColorArg(pub ColorChoice);
+
+impl ColorArg {
+    /// Allowed values the argument.
+    ///
+    /// This is useful for generating documentation via [`clap`] or `structopt`'s
+    /// `possible_values` configuration.
+    ///
+    /// [`clap`]: https://crates.io/crates/clap
+    /// [`structopt`]: https://crates.io/crates/structopt
+    pub const VARIANTS: &'static [&'static str] = &["auto", "always", "ansi", "never"];
+}
+
+impl FromStr for ColorArg {
+    type Err = &'static str;
+
+    fn from_str(src: &str) -> Result<ColorArg, &'static str> {
+        match src {
+            _ if src.eq_ignore_ascii_case("auto") => Ok(ColorArg(ColorChoice::Auto)),
+            _ if src.eq_ignore_ascii_case("always") => Ok(ColorArg(ColorChoice::Always)),
+            _ if src.eq_ignore_ascii_case("ansi") => Ok(ColorArg(ColorChoice::AlwaysAnsi)),
+            _ if src.eq_ignore_ascii_case("never") => Ok(ColorArg(ColorChoice::Never)),
+            _ => Err("valid values: auto, always, ansi, never"),
+        }
+    }
+}
+
+impl Into<ColorChoice> for ColorArg {
+    fn into(self) -> ColorChoice {
+        self.0
+    }
+}
+
+/// Emit a diagnostic using the given writer, context, config, and files.
+///
+/// The return value covers all error cases. These error case can arise if:
+/// * a file was removed from the file database.
+/// * a file was changed so that it is too small to have an index
+/// * IO fails
+pub fn emit<'files, F: Files<'files>>(
+    writer: &mut dyn WriteColor,
+    config: &Config,
+    files: &'files F,
+    diagnostic: &Diagnostic<F::FileId>,
+) -> Result<(), super::files::Error> {
+    use self::renderer::Renderer;
+    use self::views::{RichDiagnostic, ShortDiagnostic};
+
+    let mut renderer = Renderer::new(writer, config);
+    match config.display_style {
+        DisplayStyle::Rich => RichDiagnostic::new(diagnostic, config).render(files, &mut renderer),
+        DisplayStyle::Medium => ShortDiagnostic::new(diagnostic, true).render(files, &mut renderer),
+        DisplayStyle::Short => ShortDiagnostic::new(diagnostic, false).render(files, &mut renderer),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use crate::diagnostic::Label;
+    use crate::files::SimpleFiles;
+
+    #[test]
+    fn unsized_emit() {
+        let mut files = SimpleFiles::new();
+
+        let id = files.add("test", "");
+        let mut writer = termcolor::NoColor::new(Vec::<u8>::new());
+        let diagnostic = Diagnostic::bug().with_labels(vec![Label::primary(id, 0..0)]);
+
+        emit(&mut writer, &Config::default(), &files, &diagnostic).unwrap();
+    }
+}
diff --git a/src/term/config.rs b/src/term/config.rs
new file mode 100644 (file)
index 0000000..c92a6b6
--- /dev/null
@@ -0,0 +1,321 @@
+use termcolor::{Color, ColorSpec};
+
+use crate::diagnostic::{LabelStyle, Severity};
+
+/// Configures how a diagnostic is rendered.
+#[derive(Clone, Debug)]
+pub struct Config {
+    /// The display style to use when rendering diagnostics.
+    /// Defaults to: [`DisplayStyle::Rich`].
+    ///
+    /// [`DisplayStyle::Rich`]: DisplayStyle::Rich
+    pub display_style: DisplayStyle,
+    /// Column width of tabs.
+    /// Defaults to: `4`.
+    pub tab_width: usize,
+    /// Styles to use when rendering the diagnostic.
+    pub styles: Styles,
+    /// Characters to use when rendering the diagnostic.
+    pub chars: Chars,
+    /// The minimum number of lines to be shown after the line on which a multiline [`Label`] begins.
+    ///
+    /// Defaults to: `3`.
+    ///
+    /// [`Label`]: crate::diagnostic::Label
+    pub start_context_lines: usize,
+    /// The minimum number of lines to be shown before the line on which a multiline [`Label`] ends.
+    ///
+    /// Defaults to: `1`.
+    ///
+    /// [`Label`]: crate::diagnostic::Label
+    pub end_context_lines: usize,
+}
+
+impl Default for Config {
+    fn default() -> Config {
+        Config {
+            display_style: DisplayStyle::Rich,
+            tab_width: 4,
+            styles: Styles::default(),
+            chars: Chars::default(),
+            start_context_lines: 3,
+            end_context_lines: 1,
+        }
+    }
+}
+
+/// The display style to use when rendering diagnostics.
+#[derive(Clone, Debug)]
+pub enum DisplayStyle {
+    /// Output a richly formatted diagnostic, with source code previews.
+    ///
+    /// ```text
+    /// error[E0001]: unexpected type in `+` application
+    ///   ┌─ test:2:9
+    ///   │
+    /// 2 │ (+ test "")
+    ///   │         ^^ expected `Int` but found `String`
+    ///   │
+    ///   = expected type `Int`
+    ///        found type `String`
+    ///
+    /// error[E0002]: Bad config found
+    ///
+    /// ```
+    Rich,
+    /// Output a condensed diagnostic, with a line number, severity, message and notes (if any).
+    ///
+    /// ```text
+    /// test:2:9: error[E0001]: unexpected type in `+` application
+    /// = expected type `Int`
+    ///      found type `String`
+    ///
+    /// error[E0002]: Bad config found
+    /// ```
+    Medium,
+    /// Output a short diagnostic, with a line number, severity, and message.
+    ///
+    /// ```text
+    /// test:2:9: error[E0001]: unexpected type in `+` application
+    /// error[E0002]: Bad config found
+    /// ```
+    Short,
+}
+
+/// Styles to use when rendering the diagnostic.
+#[derive(Clone, Debug)]
+pub struct Styles {
+    /// The style to use when rendering bug headers.
+    /// Defaults to `fg:red bold intense`.
+    pub header_bug: ColorSpec,
+    /// The style to use when rendering error headers.
+    /// Defaults to `fg:red bold intense`.
+    pub header_error: ColorSpec,
+    /// The style to use when rendering warning headers.
+    /// Defaults to `fg:yellow bold intense`.
+    pub header_warning: ColorSpec,
+    /// The style to use when rendering note headers.
+    /// Defaults to `fg:green bold intense`.
+    pub header_note: ColorSpec,
+    /// The style to use when rendering help headers.
+    /// Defaults to `fg:cyan bold intense`.
+    pub header_help: ColorSpec,
+    /// The style to use when the main diagnostic message.
+    /// Defaults to `bold intense`.
+    pub header_message: ColorSpec,
+
+    /// The style to use when rendering bug labels.
+    /// Defaults to `fg:red`.
+    pub primary_label_bug: ColorSpec,
+    /// The style to use when rendering error labels.
+    /// Defaults to `fg:red`.
+    pub primary_label_error: ColorSpec,
+    /// The style to use when rendering warning labels.
+    /// Defaults to `fg:yellow`.
+    pub primary_label_warning: ColorSpec,
+    /// The style to use when rendering note labels.
+    /// Defaults to `fg:green`.
+    pub primary_label_note: ColorSpec,
+    /// The style to use when rendering help labels.
+    /// Defaults to `fg:cyan`.
+    pub primary_label_help: ColorSpec,
+    /// The style to use when rendering secondary labels.
+    /// Defaults `fg:blue` (or `fg:cyan` on windows).
+    pub secondary_label: ColorSpec,
+
+    /// The style to use when rendering the line numbers.
+    /// Defaults `fg:blue` (or `fg:cyan` on windows).
+    pub line_number: ColorSpec,
+    /// The style to use when rendering the source code borders.
+    /// Defaults `fg:blue` (or `fg:cyan` on windows).
+    pub source_border: ColorSpec,
+    /// The style to use when rendering the note bullets.
+    /// Defaults `fg:blue` (or `fg:cyan` on windows).
+    pub note_bullet: ColorSpec,
+}
+
+impl Styles {
+    /// The style used to mark a header at a given severity.
+    pub fn header(&self, severity: Severity) -> &ColorSpec {
+        match severity {
+            Severity::Bug => &self.header_bug,
+            Severity::Error => &self.header_error,
+            Severity::Warning => &self.header_warning,
+            Severity::Note => &self.header_note,
+            Severity::Help => &self.header_help,
+        }
+    }
+
+    /// The style used to mark a primary or secondary label at a given severity.
+    pub fn label(&self, severity: Severity, label_style: LabelStyle) -> &ColorSpec {
+        match (label_style, severity) {
+            (LabelStyle::Primary, Severity::Bug) => &self.primary_label_bug,
+            (LabelStyle::Primary, Severity::Error) => &self.primary_label_error,
+            (LabelStyle::Primary, Severity::Warning) => &self.primary_label_warning,
+            (LabelStyle::Primary, Severity::Note) => &self.primary_label_note,
+            (LabelStyle::Primary, Severity::Help) => &self.primary_label_help,
+            (LabelStyle::Secondary, _) => &self.secondary_label,
+        }
+    }
+
+    #[doc(hidden)]
+    pub fn with_blue(blue: Color) -> Styles {
+        let header = ColorSpec::new().set_bold(true).set_intense(true).clone();
+
+        Styles {
+            header_bug: header.clone().set_fg(Some(Color::Red)).clone(),
+            header_error: header.clone().set_fg(Some(Color::Red)).clone(),
+            header_warning: header.clone().set_fg(Some(Color::Yellow)).clone(),
+            header_note: header.clone().set_fg(Some(Color::Green)).clone(),
+            header_help: header.clone().set_fg(Some(Color::Cyan)).clone(),
+            header_message: header,
+
+            primary_label_bug: ColorSpec::new().set_fg(Some(Color::Red)).clone(),
+            primary_label_error: ColorSpec::new().set_fg(Some(Color::Red)).clone(),
+            primary_label_warning: ColorSpec::new().set_fg(Some(Color::Yellow)).clone(),
+            primary_label_note: ColorSpec::new().set_fg(Some(Color::Green)).clone(),
+            primary_label_help: ColorSpec::new().set_fg(Some(Color::Cyan)).clone(),
+            secondary_label: ColorSpec::new().set_fg(Some(blue)).clone(),
+
+            line_number: ColorSpec::new().set_fg(Some(blue)).clone(),
+            source_border: ColorSpec::new().set_fg(Some(blue)).clone(),
+            note_bullet: ColorSpec::new().set_fg(Some(blue)).clone(),
+        }
+    }
+}
+
+impl Default for Styles {
+    fn default() -> Styles {
+        // Blue is really difficult to see on the standard windows command line
+        #[cfg(windows)]
+        const BLUE: Color = Color::Cyan;
+        #[cfg(not(windows))]
+        const BLUE: Color = Color::Blue;
+
+        Self::with_blue(BLUE)
+    }
+}
+
+/// Characters to use when rendering the diagnostic.
+///
+/// By using [`Chars::ascii()`] you can switch to an ASCII-only format suitable
+/// for rendering on terminals that do not support box drawing characters.
+#[derive(Clone, Debug)]
+pub struct Chars {
+    /// The characters to use for the top-left border of the snippet.
+    /// Defaults to: `"┌─"` or `"-->"` with [`Chars::ascii()`].
+    pub snippet_start: String,
+    /// The character to use for the left border of the source.
+    /// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`].
+    pub source_border_left: char,
+    /// The character to use for the left border break of the source.
+    /// Defaults to: `'·'` or `'.'` with [`Chars::ascii()`].
+    pub source_border_left_break: char,
+
+    /// The character to use for the note bullet.
+    /// Defaults to: `'='`.
+    pub note_bullet: char,
+
+    /// The character to use for marking a single-line primary label.
+    /// Defaults to: `'^'`.
+    pub single_primary_caret: char,
+    /// The character to use for marking a single-line secondary label.
+    /// Defaults to: `'-'`.
+    pub single_secondary_caret: char,
+
+    /// The character to use for marking the start of a multi-line primary label.
+    /// Defaults to: `'^'`.
+    pub multi_primary_caret_start: char,
+    /// The character to use for marking the end of a multi-line primary label.
+    /// Defaults to: `'^'`.
+    pub multi_primary_caret_end: char,
+    /// The character to use for marking the start of a multi-line secondary label.
+    /// Defaults to: `'\''`.
+    pub multi_secondary_caret_start: char,
+    /// The character to use for marking the end of a multi-line secondary label.
+    /// Defaults to: `'\''`.
+    pub multi_secondary_caret_end: char,
+    /// The character to use for the top-left corner of a multi-line label.
+    /// Defaults to: `'╭'` or `'/'` with [`Chars::ascii()`].
+    pub multi_top_left: char,
+    /// The character to use for the top of a multi-line label.
+    /// Defaults to: `'─'` or `'-'` with [`Chars::ascii()`].
+    pub multi_top: char,
+    /// The character to use for the bottom-left corner of a multi-line label.
+    /// Defaults to: `'╰'` or `'\'` with [`Chars::ascii()`].
+    pub multi_bottom_left: char,
+    /// The character to use when marking the bottom of a multi-line label.
+    /// Defaults to: `'─'` or `'-'` with [`Chars::ascii()`].
+    pub multi_bottom: char,
+    /// The character to use for the left of a multi-line label.
+    /// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`].
+    pub multi_left: char,
+
+    /// The character to use for the left of a pointer underneath a caret.
+    /// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`].
+    pub pointer_left: char,
+}
+
+impl Default for Chars {
+    fn default() -> Chars {
+        Chars::box_drawing()
+    }
+}
+
+impl Chars {
+    /// A character set that uses Unicode box drawing characters.
+    pub fn box_drawing() -> Chars {
+        Chars {
+            snippet_start: "┌─".into(),
+            source_border_left: '│',
+            source_border_left_break: '·',
+
+            note_bullet: '=',
+
+            single_primary_caret: '^',
+            single_secondary_caret: '-',
+
+            multi_primary_caret_start: '^',
+            multi_primary_caret_end: '^',
+            multi_secondary_caret_start: '\'',
+            multi_secondary_caret_end: '\'',
+            multi_top_left: '╭',
+            multi_top: '─',
+            multi_bottom_left: '╰',
+            multi_bottom: '─',
+            multi_left: '│',
+
+            pointer_left: '│',
+        }
+    }
+
+    /// A character set that only uses ASCII characters.
+    ///
+    /// This is useful if your terminal's font does not support box drawing
+    /// characters well and results in output that looks similar to rustc's
+    /// diagnostic output.
+    pub fn ascii() -> Chars {
+        Chars {
+            snippet_start: "-->".into(),
+            source_border_left: '|',
+            source_border_left_break: '.',
+
+            note_bullet: '=',
+
+            single_primary_caret: '^',
+            single_secondary_caret: '-',
+
+            multi_primary_caret_start: '^',
+            multi_primary_caret_end: '^',
+            multi_secondary_caret_start: '\'',
+            multi_secondary_caret_end: '\'',
+            multi_top_left: '/',
+            multi_top: '-',
+            multi_bottom_left: '\\',
+            multi_bottom: '-',
+            multi_left: '|',
+
+            pointer_left: '|',
+        }
+    }
+}
diff --git a/src/term/renderer.rs b/src/term/renderer.rs
new file mode 100644 (file)
index 0000000..eeb8965
--- /dev/null
@@ -0,0 +1,1020 @@
+use std::io::{self, Write};
+use std::ops::Range;
+use termcolor::{ColorSpec, WriteColor};
+
+use crate::diagnostic::{LabelStyle, Severity};
+use crate::files::{Error, Location};
+use crate::term::{Chars, Config, Styles};
+
+/// The 'location focus' of a source code snippet.
+pub struct Locus {
+    /// The user-facing name of the file.
+    pub name: String,
+    /// The location.
+    pub location: Location,
+}
+
+/// Single-line label, with an optional message.
+///
+/// ```text
+/// ^^^^^^^^^ blah blah
+/// ```
+pub type SingleLabel<'diagnostic> = (LabelStyle, Range<usize>, &'diagnostic str);
+
+/// A multi-line label to render.
+///
+/// Locations are relative to the start of where the source code is rendered.
+pub enum MultiLabel<'diagnostic> {
+    /// Multi-line label top.
+    /// The contained value indicates where the label starts.
+    ///
+    /// ```text
+    /// ╭────────────^
+    /// ```
+    ///
+    /// Can also be rendered at the beginning of the line
+    /// if there is only whitespace before the label starts.
+    ///
+    /// /// ```text
+    /// ╭
+    /// ```
+    Top(usize),
+    /// Left vertical labels for multi-line labels.
+    ///
+    /// ```text
+    /// │
+    /// ```
+    Left,
+    /// Multi-line label bottom, with an optional message.
+    /// The first value indicates where the label ends.
+    ///
+    /// ```text
+    /// ╰────────────^ blah blah
+    /// ```
+    Bottom(usize, &'diagnostic str),
+}
+
+#[derive(Copy, Clone)]
+enum VerticalBound {
+    Top,
+    Bottom,
+}
+
+type Underline = (LabelStyle, VerticalBound);
+
+/// A renderer of display list entries.
+///
+/// The following diagram gives an overview of each of the parts of the renderer's output:
+///
+/// ```text
+///                     ┌ outer gutter
+///                     │ ┌ left border
+///                     │ │ ┌ inner gutter
+///                     │ │ │   ┌─────────────────────────── source ─────────────────────────────┐
+///                     │ │ │   │                                                                │
+///                  ┌────────────────────────────────────────────────────────────────────────────
+///        header ── │ error[0001]: oh noes, a cupcake has occurred!
+/// snippet start ── │    ┌─ test:9:0
+/// snippet empty ── │    │
+///  snippet line ── │  9 │   ╭ Cupcake ipsum dolor. Sit amet marshmallow topping cheesecake
+///  snippet line ── │ 10 │   │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly
+///                  │    │ ╭─│─────────^
+/// snippet break ── │    · │ │
+///  snippet line ── │ 33 │ │ │ Muffin danish chocolate soufflé pastry icing bonbon oat cake.
+///  snippet line ── │ 34 │ │ │ Powder cake jujubes oat cake. Lemon drops tootsie roll marshmallow
+///                  │    │ │ ╰─────────────────────────────^ blah blah
+/// snippet break ── │    · │
+///  snippet line ── │ 38 │ │   Brownie lemon drops chocolate jelly-o candy canes. Danish marzipan
+///  snippet line ── │ 39 │ │   jujubes soufflé carrot cake marshmallow tiramisu caramels candy canes.
+///                  │    │ │           ^^^^^^^^^^^^^^^^^^^ -------------------- blah blah
+///                  │    │ │           │
+///                  │    │ │           blah blah
+///                  │    │ │           note: this is a note
+///  snippet line ── │ 40 │ │   Fruitcake jelly-o danish toffee. Tootsie roll pastry cheesecake
+///  snippet line ── │ 41 │ │   soufflé marzipan. Chocolate bar oat cake jujubes lollipop pastry
+///  snippet line ── │ 42 │ │   cupcake. Candy canes cupcake toffee gingerbread candy canes muffin
+///                  │    │ │                                ^^^^^^^^^^^^^^^^^^ blah blah
+///                  │    │ ╰──────────^ blah blah
+/// snippet break ── │    ·
+///  snippet line ── │ 82 │     gingerbread toffee chupa chups chupa chups jelly-o cotton candy.
+///                  │    │                 ^^^^^^                         ------- blah blah
+/// snippet empty ── │    │
+///  snippet note ── │    = blah blah
+///  snippet note ── │    = blah blah blah
+///                  │      blah blah
+///  snippet note ── │    = blah blah blah
+///                  │      blah blah
+///         empty ── │
+/// ```
+///
+/// Filler text from http://www.cupcakeipsum.com
+pub struct Renderer<'writer, 'config> {
+    writer: &'writer mut dyn WriteColor,
+    config: &'config Config,
+}
+
+impl<'writer, 'config> Renderer<'writer, 'config> {
+    /// Construct a renderer from the given writer and config.
+    pub fn new(
+        writer: &'writer mut dyn WriteColor,
+        config: &'config Config,
+    ) -> Renderer<'writer, 'config> {
+        Renderer { writer, config }
+    }
+
+    fn chars(&self) -> &'config Chars {
+        &self.config.chars
+    }
+
+    fn styles(&self) -> &'config Styles {
+        &self.config.styles
+    }
+
+    /// Diagnostic header, with severity, code, and message.
+    ///
+    /// ```text
+    /// error[E0001]: unexpected type in `+` application
+    /// ```
+    pub fn render_header(
+        &mut self,
+        locus: Option<&Locus>,
+        severity: Severity,
+        code: Option<&str>,
+        message: &str,
+    ) -> Result<(), Error> {
+        // Write locus
+        //
+        // ```text
+        // test:2:9:
+        // ```
+        if let Some(locus) = locus {
+            self.snippet_locus(locus)?;
+            write!(self, ": ")?;
+        }
+
+        // Write severity name
+        //
+        // ```text
+        // error
+        // ```
+        self.set_color(self.styles().header(severity))?;
+        match severity {
+            Severity::Bug => write!(self, "bug")?,
+            Severity::Error => write!(self, "error")?,
+            Severity::Warning => write!(self, "warning")?,
+            Severity::Help => write!(self, "help")?,
+            Severity::Note => write!(self, "note")?,
+        }
+
+        // Write error code
+        //
+        // ```text
+        // [E0001]
+        // ```
+        if let Some(code) = &code.filter(|code| !code.is_empty()) {
+            write!(self, "[{}]", code)?;
+        }
+
+        // Write diagnostic message
+        //
+        // ```text
+        // : unexpected type in `+` application
+        // ```
+        self.set_color(&self.styles().header_message)?;
+        write!(self, ": {}", message)?;
+        self.reset()?;
+
+        writeln!(self)?;
+
+        Ok(())
+    }
+
+    /// Empty line.
+    pub fn render_empty(&mut self) -> Result<(), Error> {
+        writeln!(self)?;
+        Ok(())
+    }
+
+    /// Top left border and locus.
+    ///
+    /// ```text
+    /// ┌─ test:2:9
+    /// ```
+    pub fn render_snippet_start(
+        &mut self,
+        outer_padding: usize,
+        locus: &Locus,
+    ) -> Result<(), Error> {
+        self.outer_gutter(outer_padding)?;
+
+        self.set_color(&self.styles().source_border)?;
+        write!(self, "{}", self.chars().snippet_start)?;
+        self.reset()?;
+
+        write!(self, " ")?;
+        self.snippet_locus(&locus)?;
+
+        writeln!(self)?;
+
+        Ok(())
+    }
+
+    /// A line of source code.
+    ///
+    /// ```text
+    /// 10 │   │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly
+    ///    │ ╭─│─────────^
+    /// ```
+    pub fn render_snippet_source(
+        &mut self,
+        outer_padding: usize,
+        line_number: usize,
+        source: &str,
+        severity: Severity,
+        single_labels: &[SingleLabel<'_>],
+        num_multi_labels: usize,
+        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
+    ) -> Result<(), Error> {
+        // Trim trailing newlines, linefeeds, and null chars from source, if they exist.
+        // FIXME: Use the number of trimmed placeholders when rendering single line carets
+        let source = source.trim_end_matches(['\n', '\r', '\0'].as_ref());
+
+        // Write source line
+        //
+        // ```text
+        // 10 │   │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly
+        // ```
+        {
+            // Write outer gutter (with line number) and border
+            self.outer_gutter_number(line_number, outer_padding)?;
+            self.border_left()?;
+
+            // Write inner gutter (with multi-line continuations on the left if necessary)
+            let mut multi_labels_iter = multi_labels.iter().peekable();
+            for label_column in 0..num_multi_labels {
+                match multi_labels_iter.peek() {
+                    Some((label_index, label_style, label)) if *label_index == label_column => {
+                        match label {
+                            MultiLabel::Top(start)
+                                if *start <= source.len() - source.trim_start().len() =>
+                            {
+                                self.label_multi_top_left(severity, *label_style)?;
+                            }
+                            MultiLabel::Top(..) => self.inner_gutter_space()?,
+                            MultiLabel::Left | MultiLabel::Bottom(..) => {
+                                self.label_multi_left(severity, *label_style, None)?;
+                            }
+                        }
+                        multi_labels_iter.next();
+                    }
+                    Some((_, _, _)) | None => self.inner_gutter_space()?,
+                }
+            }
+
+            // Write source text
+            write!(self, " ")?;
+            let mut in_primary = false;
+            for (metrics, ch) in self.char_metrics(source.char_indices()) {
+                let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
+
+                // Check if we are overlapping a primary label
+                let is_primary = single_labels.iter().any(|(ls, range, _)| {
+                    *ls == LabelStyle::Primary && is_overlapping(range, &column_range)
+                }) || multi_labels.iter().any(|(_, ls, label)| {
+                    *ls == LabelStyle::Primary
+                        && match label {
+                            MultiLabel::Top(start) => column_range.start >= *start,
+                            MultiLabel::Left => true,
+                            MultiLabel::Bottom(start, _) => column_range.end <= *start,
+                        }
+                });
+
+                // Set the source color if we are in a primary label
+                if is_primary && !in_primary {
+                    self.set_color(self.styles().label(severity, LabelStyle::Primary))?;
+                    in_primary = true;
+                } else if !is_primary && in_primary {
+                    self.reset()?;
+                    in_primary = false;
+                }
+
+                match ch {
+                    '\t' => (0..metrics.unicode_width).try_for_each(|_| write!(self, " "))?,
+                    _ => write!(self, "{}", ch)?,
+                }
+            }
+            if in_primary {
+                self.reset()?;
+            }
+            writeln!(self)?;
+        }
+
+        // Write single labels underneath source
+        //
+        // ```text
+        //   │     - ---- ^^^ second mutable borrow occurs here
+        //   │     │ │
+        //   │     │ first mutable borrow occurs here
+        //   │     first borrow later used by call
+        //   │     help: some help here
+        // ```
+        if !single_labels.is_empty() {
+            // Our plan is as follows:
+            //
+            // 1. Do an initial scan to find:
+            //    - The number of non-empty messages.
+            //    - The right-most start and end positions of labels.
+            //    - A candidate for a trailing label (where the label's message
+            //      is printed to the left of the caret).
+            // 2. Check if the trailing label candidate overlaps another label -
+            //    if so we print it underneath the carets with the other labels.
+            // 3. Print a line of carets, and (possibly) the trailing message
+            //    to the left.
+            // 4. Print vertical lines pointing to the carets, and the messages
+            //    for those carets.
+            //
+            // We try our best avoid introducing new dynamic allocations,
+            // instead preferring to iterate over the labels multiple times. It
+            // is unclear what the performance tradeoffs are however, so further
+            // investigation may be required.
+
+            // The number of non-empty messages to print.
+            let mut num_messages = 0;
+            // The right-most start position, eg:
+            //
+            // ```text
+            // -^^^^---- ^^^^^^^
+            //           │
+            //           right-most start position
+            // ```
+            let mut max_label_start = 0;
+            // The right-most end position, eg:
+            //
+            // ```text
+            // -^^^^---- ^^^^^^^
+            //                 │
+            //                 right-most end position
+            // ```
+            let mut max_label_end = 0;
+            // A trailing message, eg:
+            //
+            // ```text
+            // ^^^ second mutable borrow occurs here
+            // ```
+            let mut trailing_label = None;
+
+            for (label_index, label) in single_labels.iter().enumerate() {
+                let (_, range, message) = label;
+                if !message.is_empty() {
+                    num_messages += 1;
+                }
+                max_label_start = std::cmp::max(max_label_start, range.start);
+                max_label_end = std::cmp::max(max_label_end, range.end);
+                // This is a candidate for the trailing label, so let's record it.
+                if range.end == max_label_end {
+                    if message.is_empty() {
+                        trailing_label = None;
+                    } else {
+                        trailing_label = Some((label_index, label));
+                    }
+                }
+            }
+            if let Some((trailing_label_index, (_, trailing_range, _))) = trailing_label {
+                // Check to see if the trailing label candidate overlaps any of
+                // the other labels on the current line.
+                if single_labels
+                    .iter()
+                    .enumerate()
+                    .filter(|(label_index, _)| *label_index != trailing_label_index)
+                    .any(|(_, (_, range, _))| is_overlapping(trailing_range, range))
+                {
+                    // If it does, we'll instead want to render it below the
+                    // carets along with the other hanging labels.
+                    trailing_label = None;
+                }
+            }
+
+            // Write a line of carets
+            //
+            // ```text
+            //   │ ^^^^^^  -------^^^^^^^^^-------^^^^^----- ^^^^ trailing label message
+            // ```
+            self.outer_gutter(outer_padding)?;
+            self.border_left()?;
+            self.inner_gutter(severity, num_multi_labels, multi_labels)?;
+            write!(self, " ")?;
+
+            let mut previous_label_style = None;
+            let placeholder_metrics = Metrics {
+                byte_index: source.len(),
+                unicode_width: 1,
+            };
+            for (metrics, ch) in self
+                .char_metrics(source.char_indices())
+                // Add a placeholder source column at the end to allow for
+                // printing carets at the end of lines, eg:
+                //
+                // ```text
+                // 1 │ Hello world!
+                //   │             ^
+                // ```
+                .chain(std::iter::once((placeholder_metrics, '\0')))
+            {
+                // Find the current label style at this column
+                let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
+                let current_label_style = single_labels
+                    .iter()
+                    .filter(|(_, range, _)| is_overlapping(range, &column_range))
+                    .map(|(label_style, _, _)| *label_style)
+                    .max_by_key(label_priority_key);
+
+                // Update writer style if necessary
+                if previous_label_style != current_label_style {
+                    match current_label_style {
+                        None => self.reset()?,
+                        Some(label_style) => {
+                            self.set_color(self.styles().label(severity, label_style))?;
+                        }
+                    }
+                }
+
+                let caret_ch = match current_label_style {
+                    Some(LabelStyle::Primary) => Some(self.chars().single_primary_caret),
+                    Some(LabelStyle::Secondary) => Some(self.chars().single_secondary_caret),
+                    // Only print padding if we are before the end of the last single line caret
+                    None if metrics.byte_index < max_label_end => Some(' '),
+                    None => None,
+                };
+                if let Some(caret_ch) = caret_ch {
+                    // FIXME: improve rendering of carets between character boundaries
+                    (0..metrics.unicode_width).try_for_each(|_| write!(self, "{}", caret_ch))?;
+                }
+
+                previous_label_style = current_label_style;
+            }
+            // Reset style if it was previously set
+            if previous_label_style.is_some() {
+                self.reset()?;
+            }
+            // Write first trailing label message
+            if let Some((_, (label_style, _, message))) = trailing_label {
+                write!(self, " ")?;
+                self.set_color(self.styles().label(severity, *label_style))?;
+                write!(self, "{}", message)?;
+                self.reset()?;
+            }
+            writeln!(self)?;
+
+            // Write hanging labels pointing to carets
+            //
+            // ```text
+            //   │     │ │
+            //   │     │ first mutable borrow occurs here
+            //   │     first borrow later used by call
+            //   │     help: some help here
+            // ```
+            if num_messages > trailing_label.iter().count() {
+                // Write first set of vertical lines before hanging labels
+                //
+                // ```text
+                //   │     │ │
+                // ```
+                self.outer_gutter(outer_padding)?;
+                self.border_left()?;
+                self.inner_gutter(severity, num_multi_labels, multi_labels)?;
+                write!(self, " ")?;
+                self.caret_pointers(
+                    severity,
+                    max_label_start,
+                    single_labels,
+                    trailing_label,
+                    source.char_indices(),
+                )?;
+                writeln!(self)?;
+
+                // Write hanging labels pointing to carets
+                //
+                // ```text
+                //   │     │ first mutable borrow occurs here
+                //   │     first borrow later used by call
+                //   │     help: some help here
+                // ```
+                for (label_style, range, message) in
+                    hanging_labels(single_labels, trailing_label).rev()
+                {
+                    self.outer_gutter(outer_padding)?;
+                    self.border_left()?;
+                    self.inner_gutter(severity, num_multi_labels, multi_labels)?;
+                    write!(self, " ")?;
+                    self.caret_pointers(
+                        severity,
+                        max_label_start,
+                        single_labels,
+                        trailing_label,
+                        source
+                            .char_indices()
+                            .take_while(|(byte_index, _)| *byte_index < range.start),
+                    )?;
+                    self.set_color(self.styles().label(severity, *label_style))?;
+                    write!(self, "{}", message)?;
+                    self.reset()?;
+                    writeln!(self)?;
+                }
+            }
+        }
+
+        // Write top or bottom label carets underneath source
+        //
+        // ```text
+        //     │ ╰───│──────────────────^ woops
+        //     │   ╭─│─────────^
+        // ```
+        for (multi_label_index, (_, label_style, label)) in multi_labels.iter().enumerate() {
+            let (label_style, range, bottom_message) = match label {
+                MultiLabel::Left => continue, // no label caret needed
+                // no label caret needed if this can be started in front of the line
+                MultiLabel::Top(start) if *start <= source.len() - source.trim_start().len() => {
+                    continue
+                }
+                MultiLabel::Top(range) => (*label_style, range, None),
+                MultiLabel::Bottom(range, message) => (*label_style, range, Some(message)),
+            };
+
+            self.outer_gutter(outer_padding)?;
+            self.border_left()?;
+
+            // Write inner gutter.
+            //
+            // ```text
+            //  │ ╭─│───│
+            // ```
+            let mut underline = None;
+            let mut multi_labels_iter = multi_labels.iter().enumerate().peekable();
+            for label_column in 0..num_multi_labels {
+                match multi_labels_iter.peek() {
+                    Some((i, (label_index, ls, label))) if *label_index == label_column => {
+                        match label {
+                            MultiLabel::Left => {
+                                self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
+                            }
+                            MultiLabel::Top(..) if multi_label_index > *i => {
+                                self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
+                            }
+                            MultiLabel::Bottom(..) if multi_label_index < *i => {
+                                self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
+                            }
+                            MultiLabel::Top(..) if multi_label_index == *i => {
+                                underline = Some((*ls, VerticalBound::Top));
+                                self.label_multi_top_left(severity, label_style)?
+                            }
+                            MultiLabel::Bottom(..) if multi_label_index == *i => {
+                                underline = Some((*ls, VerticalBound::Bottom));
+                                self.label_multi_bottom_left(severity, label_style)?;
+                            }
+                            MultiLabel::Top(..) | MultiLabel::Bottom(..) => {
+                                self.inner_gutter_column(severity, underline)?;
+                            }
+                        }
+                        multi_labels_iter.next();
+                    }
+                    Some((_, _)) | None => self.inner_gutter_column(severity, underline)?,
+                }
+            }
+
+            // Finish the top or bottom caret
+            match bottom_message {
+                None => self.label_multi_top_caret(severity, label_style, source, *range)?,
+                Some(message) => {
+                    self.label_multi_bottom_caret(severity, label_style, source, *range, message)?
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    /// An empty source line, for providing additional whitespace to source snippets.
+    ///
+    /// ```text
+    /// │ │ │
+    /// ```
+    pub fn render_snippet_empty(
+        &mut self,
+        outer_padding: usize,
+        severity: Severity,
+        num_multi_labels: usize,
+        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
+    ) -> Result<(), Error> {
+        self.outer_gutter(outer_padding)?;
+        self.border_left()?;
+        self.inner_gutter(severity, num_multi_labels, multi_labels)?;
+        writeln!(self)?;
+        Ok(())
+    }
+
+    /// A broken source line, for labeling skipped sections of source.
+    ///
+    /// ```text
+    /// · │ │
+    /// ```
+    pub fn render_snippet_break(
+        &mut self,
+        outer_padding: usize,
+        severity: Severity,
+        num_multi_labels: usize,
+        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
+    ) -> Result<(), Error> {
+        self.outer_gutter(outer_padding)?;
+        self.border_left_break()?;
+        self.inner_gutter(severity, num_multi_labels, multi_labels)?;
+        writeln!(self)?;
+        Ok(())
+    }
+
+    /// Additional notes.
+    ///
+    /// ```text
+    /// = expected type `Int`
+    ///      found type `String`
+    /// ```
+    pub fn render_snippet_note(
+        &mut self,
+        outer_padding: usize,
+        message: &str,
+    ) -> Result<(), Error> {
+        for (note_line_index, line) in message.lines().enumerate() {
+            self.outer_gutter(outer_padding)?;
+            match note_line_index {
+                0 => {
+                    self.set_color(&self.styles().note_bullet)?;
+                    write!(self, "{}", self.chars().note_bullet)?;
+                    self.reset()?;
+                }
+                _ => write!(self, " ")?,
+            }
+            // Write line of message
+            writeln!(self, " {}", line)?;
+        }
+
+        Ok(())
+    }
+
+    /// Adds tab-stop aware unicode-width computations to an iterator over
+    /// character indices. Assumes that the character indices begin at the start
+    /// of the line.
+    fn char_metrics(
+        &self,
+        char_indices: impl Iterator<Item = (usize, char)>,
+    ) -> impl Iterator<Item = (Metrics, char)> {
+        use unicode_width::UnicodeWidthChar;
+
+        let tab_width = self.config.tab_width;
+        let mut unicode_column = 0;
+
+        char_indices.map(move |(byte_index, ch)| {
+            let metrics = Metrics {
+                byte_index,
+                unicode_width: match (ch, tab_width) {
+                    ('\t', 0) => 0, // Guard divide-by-zero
+                    ('\t', _) => tab_width - (unicode_column % tab_width),
+                    (ch, _) => ch.width().unwrap_or(0),
+                },
+            };
+            unicode_column += metrics.unicode_width;
+
+            (metrics, ch)
+        })
+    }
+
+    /// Location focus.
+    fn snippet_locus(&mut self, locus: &Locus) -> Result<(), Error> {
+        write!(
+            self,
+            "{name}:{line_number}:{column_number}",
+            name = locus.name,
+            line_number = locus.location.line_number,
+            column_number = locus.location.column_number,
+        )?;
+        Ok(())
+    }
+
+    /// The outer gutter of a source line.
+    fn outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error> {
+        write!(self, "{space: >width$} ", space = "", width = outer_padding)?;
+        Ok(())
+    }
+
+    /// The outer gutter of a source line, with line number.
+    fn outer_gutter_number(
+        &mut self,
+        line_number: usize,
+        outer_padding: usize,
+    ) -> Result<(), Error> {
+        self.set_color(&self.styles().line_number)?;
+        write!(
+            self,
+            "{line_number: >width$}",
+            line_number = line_number,
+            width = outer_padding,
+        )?;
+        self.reset()?;
+        write!(self, " ")?;
+        Ok(())
+    }
+
+    /// The left-hand border of a source line.
+    fn border_left(&mut self) -> Result<(), Error> {
+        self.set_color(&self.styles().source_border)?;
+        write!(self, "{}", self.chars().source_border_left)?;
+        self.reset()?;
+        Ok(())
+    }
+
+    /// The broken left-hand border of a source line.
+    fn border_left_break(&mut self) -> Result<(), Error> {
+        self.set_color(&self.styles().source_border)?;
+        write!(self, "{}", self.chars().source_border_left_break)?;
+        self.reset()?;
+        Ok(())
+    }
+
+    /// Write vertical lines pointing to carets.
+    fn caret_pointers(
+        &mut self,
+        severity: Severity,
+        max_label_start: usize,
+        single_labels: &[SingleLabel<'_>],
+        trailing_label: Option<(usize, &SingleLabel<'_>)>,
+        char_indices: impl Iterator<Item = (usize, char)>,
+    ) -> Result<(), Error> {
+        for (metrics, ch) in self.char_metrics(char_indices) {
+            let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
+            let label_style = hanging_labels(single_labels, trailing_label)
+                .filter(|(_, range, _)| column_range.contains(&range.start))
+                .map(|(label_style, _, _)| *label_style)
+                .max_by_key(label_priority_key);
+
+            let mut spaces = match label_style {
+                None => 0..metrics.unicode_width,
+                Some(label_style) => {
+                    self.set_color(self.styles().label(severity, label_style))?;
+                    write!(self, "{}", self.chars().pointer_left)?;
+                    self.reset()?;
+                    1..metrics.unicode_width
+                }
+            };
+            // Only print padding if we are before the end of the last single line caret
+            if metrics.byte_index <= max_label_start {
+                spaces.try_for_each(|_| write!(self, " "))?;
+            }
+        }
+
+        Ok(())
+    }
+
+    /// The left of a multi-line label.
+    ///
+    /// ```text
+    ///  │
+    /// ```
+    fn label_multi_left(
+        &mut self,
+        severity: Severity,
+        label_style: LabelStyle,
+        underline: Option<LabelStyle>,
+    ) -> Result<(), Error> {
+        match underline {
+            None => write!(self, " ")?,
+            // Continue an underline horizontally
+            Some(label_style) => {
+                self.set_color(self.styles().label(severity, label_style))?;
+                write!(self, "{}", self.chars().multi_top)?;
+                self.reset()?;
+            }
+        }
+        self.set_color(self.styles().label(severity, label_style))?;
+        write!(self, "{}", self.chars().multi_left)?;
+        self.reset()?;
+        Ok(())
+    }
+
+    /// The top-left of a multi-line label.
+    ///
+    /// ```text
+    ///  ╭
+    /// ```
+    fn label_multi_top_left(
+        &mut self,
+        severity: Severity,
+        label_style: LabelStyle,
+    ) -> Result<(), Error> {
+        write!(self, " ")?;
+        self.set_color(self.styles().label(severity, label_style))?;
+        write!(self, "{}", self.chars().multi_top_left)?;
+        self.reset()?;
+        Ok(())
+    }
+
+    /// The bottom left of a multi-line label.
+    ///
+    /// ```text
+    ///  ╰
+    /// ```
+    fn label_multi_bottom_left(
+        &mut self,
+        severity: Severity,
+        label_style: LabelStyle,
+    ) -> Result<(), Error> {
+        write!(self, " ")?;
+        self.set_color(self.styles().label(severity, label_style))?;
+        write!(self, "{}", self.chars().multi_bottom_left)?;
+        self.reset()?;
+        Ok(())
+    }
+
+    /// Multi-line label top.
+    ///
+    /// ```text
+    /// ─────────────^
+    /// ```
+    fn label_multi_top_caret(
+        &mut self,
+        severity: Severity,
+        label_style: LabelStyle,
+        source: &str,
+        start: usize,
+    ) -> Result<(), Error> {
+        self.set_color(self.styles().label(severity, label_style))?;
+
+        for (metrics, _) in self
+            .char_metrics(source.char_indices())
+            .take_while(|(metrics, _)| metrics.byte_index < start + 1)
+        {
+            // FIXME: improve rendering of carets between character boundaries
+            (0..metrics.unicode_width)
+                .try_for_each(|_| write!(self, "{}", self.chars().multi_top))?;
+        }
+
+        let caret_start = match label_style {
+            LabelStyle::Primary => self.config.chars.multi_primary_caret_start,
+            LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start,
+        };
+        write!(self, "{}", caret_start)?;
+        self.reset()?;
+        writeln!(self)?;
+        Ok(())
+    }
+
+    /// Multi-line label bottom, with a message.
+    ///
+    /// ```text
+    /// ─────────────^ expected `Int` but found `String`
+    /// ```
+    fn label_multi_bottom_caret(
+        &mut self,
+        severity: Severity,
+        label_style: LabelStyle,
+        source: &str,
+        start: usize,
+        message: &str,
+    ) -> Result<(), Error> {
+        self.set_color(self.styles().label(severity, label_style))?;
+
+        for (metrics, _) in self
+            .char_metrics(source.char_indices())
+            .take_while(|(metrics, _)| metrics.byte_index < start)
+        {
+            // FIXME: improve rendering of carets between character boundaries
+            (0..metrics.unicode_width)
+                .try_for_each(|_| write!(self, "{}", self.chars().multi_bottom))?;
+        }
+
+        let caret_end = match label_style {
+            LabelStyle::Primary => self.config.chars.multi_primary_caret_start,
+            LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start,
+        };
+        write!(self, "{}", caret_end)?;
+        if !message.is_empty() {
+            write!(self, " {}", message)?;
+        }
+        self.reset()?;
+        writeln!(self)?;
+        Ok(())
+    }
+
+    /// Writes an empty gutter space, or continues an underline horizontally.
+    fn inner_gutter_column(
+        &mut self,
+        severity: Severity,
+        underline: Option<Underline>,
+    ) -> Result<(), Error> {
+        match underline {
+            None => self.inner_gutter_space(),
+            Some((label_style, vertical_bound)) => {
+                self.set_color(self.styles().label(severity, label_style))?;
+                let ch = match vertical_bound {
+                    VerticalBound::Top => self.config.chars.multi_top,
+                    VerticalBound::Bottom => self.config.chars.multi_bottom,
+                };
+                write!(self, "{0}{0}", ch)?;
+                self.reset()?;
+                Ok(())
+            }
+        }
+    }
+
+    /// Writes an empty gutter space.
+    fn inner_gutter_space(&mut self) -> Result<(), Error> {
+        write!(self, "  ")?;
+        Ok(())
+    }
+
+    /// Writes an inner gutter, with the left lines if necessary.
+    fn inner_gutter(
+        &mut self,
+        severity: Severity,
+        num_multi_labels: usize,
+        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
+    ) -> Result<(), Error> {
+        let mut multi_labels_iter = multi_labels.iter().peekable();
+        for label_column in 0..num_multi_labels {
+            match multi_labels_iter.peek() {
+                Some((label_index, ls, label)) if *label_index == label_column => match label {
+                    MultiLabel::Left | MultiLabel::Bottom(..) => {
+                        self.label_multi_left(severity, *ls, None)?;
+                        multi_labels_iter.next();
+                    }
+                    MultiLabel::Top(..) => {
+                        self.inner_gutter_space()?;
+                        multi_labels_iter.next();
+                    }
+                },
+                Some((_, _, _)) | None => self.inner_gutter_space()?,
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl<'writer, 'config> Write for Renderer<'writer, 'config> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.writer.write(buf)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.writer.flush()
+    }
+}
+
+impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> {
+    fn supports_color(&self) -> bool {
+        self.writer.supports_color()
+    }
+
+    fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
+        self.writer.set_color(spec)
+    }
+
+    fn reset(&mut self) -> io::Result<()> {
+        self.writer.reset()
+    }
+
+    fn is_synchronous(&self) -> bool {
+        self.writer.is_synchronous()
+    }
+}
+
+struct Metrics {
+    byte_index: usize,
+    unicode_width: usize,
+}
+
+/// Check if two ranges overlap
+fn is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> bool {
+    let start = std::cmp::max(range0.start, range1.start);
+    let end = std::cmp::min(range0.end, range1.end);
+    start < end
+}
+
+/// For prioritizing primary labels over secondary labels when rendering carets.
+fn label_priority_key(label_style: &LabelStyle) -> u8 {
+    match label_style {
+        LabelStyle::Secondary => 0,
+        LabelStyle::Primary => 1,
+    }
+}
+
+/// Return an iterator that yields the labels that require hanging messages
+/// rendered underneath them.
+fn hanging_labels<'labels, 'diagnostic>(
+    single_labels: &'labels [SingleLabel<'diagnostic>],
+    trailing_label: Option<(usize, &'labels SingleLabel<'diagnostic>)>,
+) -> impl 'labels + DoubleEndedIterator<Item = &'labels SingleLabel<'diagnostic>> {
+    single_labels
+        .iter()
+        .enumerate()
+        .filter(|(_, (_, _, message))| !message.is_empty())
+        .filter(move |(i, _)| trailing_label.map_or(true, |(j, _)| *i != j))
+        .map(|(_, label)| label)
+}
diff --git a/src/term/views.rs b/src/term/views.rs
new file mode 100644 (file)
index 0000000..f09d958
--- /dev/null
@@ -0,0 +1,478 @@
+use std::ops::Range;
+
+use crate::diagnostic::{Diagnostic, LabelStyle};
+use crate::files::{Error, Files, Location};
+use crate::term::renderer::{Locus, MultiLabel, Renderer, SingleLabel};
+use crate::term::Config;
+
+/// Count the number of decimal digits in `n`.
+fn count_digits(mut n: usize) -> usize {
+    let mut count = 0;
+    while n != 0 {
+        count += 1;
+        n /= 10; // remove last digit
+    }
+    count
+}
+
+/// Output a richly formatted diagnostic, with source code previews.
+pub struct RichDiagnostic<'diagnostic, 'config, FileId> {
+    diagnostic: &'diagnostic Diagnostic<FileId>,
+    config: &'config Config,
+}
+
+impl<'diagnostic, 'config, FileId> RichDiagnostic<'diagnostic, 'config, FileId>
+where
+    FileId: Copy + PartialEq,
+{
+    pub fn new(
+        diagnostic: &'diagnostic Diagnostic<FileId>,
+        config: &'config Config,
+    ) -> RichDiagnostic<'diagnostic, 'config, FileId> {
+        RichDiagnostic { diagnostic, config }
+    }
+
+    pub fn render<'files>(
+        &self,
+        files: &'files impl Files<'files, FileId = FileId>,
+        renderer: &mut Renderer<'_, '_>,
+    ) -> Result<(), Error>
+    where
+        FileId: 'files,
+    {
+        use std::collections::BTreeMap;
+
+        struct LabeledFile<'diagnostic, FileId> {
+            file_id: FileId,
+            start: usize,
+            name: String,
+            location: Location,
+            num_multi_labels: usize,
+            lines: BTreeMap<usize, Line<'diagnostic>>,
+            max_label_style: LabelStyle,
+        }
+
+        impl<'diagnostic, FileId> LabeledFile<'diagnostic, FileId> {
+            fn get_or_insert_line(
+                &mut self,
+                line_index: usize,
+                line_range: Range<usize>,
+                line_number: usize,
+            ) -> &mut Line<'diagnostic> {
+                self.lines.entry(line_index).or_insert_with(|| Line {
+                    range: line_range,
+                    number: line_number,
+                    single_labels: vec![],
+                    multi_labels: vec![],
+                    // This has to be false by default so we know if it must be rendered by another condition already.
+                    must_render: false,
+                })
+            }
+        }
+
+        struct Line<'diagnostic> {
+            number: usize,
+            range: std::ops::Range<usize>,
+            // TODO: How do we reuse these allocations?
+            single_labels: Vec<SingleLabel<'diagnostic>>,
+            multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>,
+            must_render: bool,
+        }
+
+        // TODO: Make this data structure external, to allow for allocation reuse
+        let mut labeled_files = Vec::<LabeledFile<'_, _>>::new();
+        // Keep track of the outer padding to use when rendering the
+        // snippets of source code.
+        let mut outer_padding = 0;
+
+        // Group labels by file
+        for label in &self.diagnostic.labels {
+            let start_line_index = files.line_index(label.file_id, label.range.start)?;
+            let start_line_number = files.line_number(label.file_id, start_line_index)?;
+            let start_line_range = files.line_range(label.file_id, start_line_index)?;
+            let end_line_index = files.line_index(label.file_id, label.range.end)?;
+            let end_line_number = files.line_number(label.file_id, end_line_index)?;
+            let end_line_range = files.line_range(label.file_id, end_line_index)?;
+
+            outer_padding = std::cmp::max(outer_padding, count_digits(start_line_number));
+            outer_padding = std::cmp::max(outer_padding, count_digits(end_line_number));
+
+            // NOTE: This could be made more efficient by using an associative
+            // data structure like a hashmap or B-tree,  but we use a vector to
+            // preserve the order that unique files appear in the list of labels.
+            let labeled_file = match labeled_files
+                .iter_mut()
+                .find(|labeled_file| label.file_id == labeled_file.file_id)
+            {
+                Some(labeled_file) => {
+                    // another diagnostic also referenced this file
+                    if labeled_file.max_label_style > label.style
+                        || (labeled_file.max_label_style == label.style
+                            && labeled_file.start > label.range.start)
+                    {
+                        // this label has a higher style or has the same style but starts earlier
+                        labeled_file.start = label.range.start;
+                        labeled_file.location = files.location(label.file_id, label.range.start)?;
+                        labeled_file.max_label_style = label.style;
+                    }
+                    labeled_file
+                }
+                None => {
+                    // no other diagnostic referenced this file yet
+                    labeled_files.push(LabeledFile {
+                        file_id: label.file_id,
+                        start: label.range.start,
+                        name: files.name(label.file_id)?.to_string(),
+                        location: files.location(label.file_id, label.range.start)?,
+                        num_multi_labels: 0,
+                        lines: BTreeMap::new(),
+                        max_label_style: label.style,
+                    });
+                    // this unwrap should never fail because we just pushed an element
+                    labeled_files
+                        .last_mut()
+                        .expect("just pushed an element that disappeared")
+                }
+            };
+
+            if start_line_index == end_line_index {
+                // Single line
+                //
+                // ```text
+                // 2 │ (+ test "")
+                //   │         ^^ expected `Int` but found `String`
+                // ```
+                let label_start = label.range.start - start_line_range.start;
+                // Ensure that we print at least one caret, even when we
+                // have a zero-length source range.
+                let label_end =
+                    usize::max(label.range.end - start_line_range.start, label_start + 1);
+
+                let line = labeled_file.get_or_insert_line(
+                    start_line_index,
+                    start_line_range,
+                    start_line_number,
+                );
+
+                // Ensure that the single line labels are lexicographically
+                // sorted by the range of source code that they cover.
+                let index = match line.single_labels.binary_search_by(|(_, range, _)| {
+                    // `Range<usize>` doesn't implement `Ord`, so convert to `(usize, usize)`
+                    // to piggyback off its lexicographic comparison implementation.
+                    (range.start, range.end).cmp(&(label_start, label_end))
+                }) {
+                    // If the ranges are the same, order the labels in reverse
+                    // to how they were originally specified in the diagnostic.
+                    // This helps with printing in the renderer.
+                    Ok(index) | Err(index) => index,
+                };
+
+                line.single_labels
+                    .insert(index, (label.style, label_start..label_end, &label.message));
+
+                // If this line is not rendered, the SingleLabel is not visible.
+                line.must_render = true;
+            } else {
+                // Multiple lines
+                //
+                // ```text
+                // 4 │   fizz₁ num = case (mod num 5) (mod num 3) of
+                //   │ ╭─────────────^
+                // 5 │ │     0 0 => "FizzBuzz"
+                // 6 │ │     0 _ => "Fizz"
+                // 7 │ │     _ 0 => "Buzz"
+                // 8 │ │     _ _ => num
+                //   │ ╰──────────────^ `case` clauses have incompatible types
+                // ```
+
+                let label_index = labeled_file.num_multi_labels;
+                labeled_file.num_multi_labels += 1;
+
+                // First labeled line
+                let label_start = label.range.start - start_line_range.start;
+
+                let start_line = labeled_file.get_or_insert_line(
+                    start_line_index,
+                    start_line_range.clone(),
+                    start_line_number,
+                );
+
+                start_line.multi_labels.push((
+                    label_index,
+                    label.style,
+                    MultiLabel::Top(label_start),
+                ));
+
+                // The first line has to be rendered so the start of the label is visible.
+                start_line.must_render = true;
+
+                // Marked lines
+                //
+                // ```text
+                // 5 │ │     0 0 => "FizzBuzz"
+                // 6 │ │     0 _ => "Fizz"
+                // 7 │ │     _ 0 => "Buzz"
+                // ```
+                for line_index in (start_line_index + 1)..end_line_index {
+                    let line_range = files.line_range(label.file_id, line_index)?;
+                    let line_number = files.line_number(label.file_id, line_index)?;
+
+                    outer_padding = std::cmp::max(outer_padding, count_digits(line_number));
+
+                    let line = labeled_file.get_or_insert_line(line_index, line_range, line_number);
+
+                    line.multi_labels
+                        .push((label_index, label.style, MultiLabel::Left));
+
+                    // The line should be rendered to match the configuration of how much context to show.
+                    line.must_render |=
+                        // Is this line part of the context after the start of the label?
+                        line_index - start_line_index <= self.config.start_context_lines
+                        ||
+                        // Is this line part of the context before the end of the label?
+                        end_line_index - line_index <= self.config.end_context_lines;
+                }
+
+                // Last labeled line
+                //
+                // ```text
+                // 8 │ │     _ _ => num
+                //   │ ╰──────────────^ `case` clauses have incompatible types
+                // ```
+                let label_end = label.range.end - end_line_range.start;
+
+                let end_line = labeled_file.get_or_insert_line(
+                    end_line_index,
+                    end_line_range,
+                    end_line_number,
+                );
+
+                end_line.multi_labels.push((
+                    label_index,
+                    label.style,
+                    MultiLabel::Bottom(label_end, &label.message),
+                ));
+
+                // The last line has to be rendered so the end of the label is visible.
+                end_line.must_render = true;
+            }
+        }
+
+        // Header and message
+        //
+        // ```text
+        // error[E0001]: unexpected type in `+` application
+        // ```
+        renderer.render_header(
+            None,
+            self.diagnostic.severity,
+            self.diagnostic.code.as_deref(),
+            self.diagnostic.message.as_str(),
+        )?;
+
+        // Source snippets
+        //
+        // ```text
+        //   ┌─ test:2:9
+        //   │
+        // 2 │ (+ test "")
+        //   │         ^^ expected `Int` but found `String`
+        //   │
+        // ```
+        let mut labeled_files = labeled_files.into_iter().peekable();
+        while let Some(labeled_file) = labeled_files.next() {
+            let source = files.source(labeled_file.file_id)?;
+            let source = source.as_ref();
+
+            // Top left border and locus.
+            //
+            // ```text
+            // ┌─ test:2:9
+            // ```
+            if !labeled_file.lines.is_empty() {
+                renderer.render_snippet_start(
+                    outer_padding,
+                    &Locus {
+                        name: labeled_file.name,
+                        location: labeled_file.location,
+                    },
+                )?;
+                renderer.render_snippet_empty(
+                    outer_padding,
+                    self.diagnostic.severity,
+                    labeled_file.num_multi_labels,
+                    &[],
+                )?;
+            }
+
+            let mut lines = labeled_file
+                .lines
+                .iter()
+                .filter(|(_, line)| line.must_render)
+                .peekable();
+
+            while let Some((line_index, line)) = lines.next() {
+                renderer.render_snippet_source(
+                    outer_padding,
+                    line.number,
+                    &source[line.range.clone()],
+                    self.diagnostic.severity,
+                    &line.single_labels,
+                    labeled_file.num_multi_labels,
+                    &line.multi_labels,
+                )?;
+
+                // Check to see if we need to render any intermediate stuff
+                // before rendering the next line.
+                if let Some((next_line_index, _)) = lines.peek() {
+                    match next_line_index.checked_sub(*line_index) {
+                        // Consecutive lines
+                        Some(1) => {}
+                        // One line between the current line and the next line
+                        Some(2) => {
+                            // Write a source line
+                            let file_id = labeled_file.file_id;
+
+                            // This line was not intended to be rendered initially.
+                            // To render the line right, we have to get back the original labels.
+                            let labels = labeled_file
+                                .lines
+                                .get(&(line_index + 1))
+                                .map_or(&[][..], |line| &line.multi_labels[..]);
+
+                            renderer.render_snippet_source(
+                                outer_padding,
+                                files.line_number(file_id, line_index + 1)?,
+                                &source[files.line_range(file_id, line_index + 1)?],
+                                self.diagnostic.severity,
+                                &[],
+                                labeled_file.num_multi_labels,
+                                labels,
+                            )?;
+                        }
+                        // More than one line between the current line and the next line.
+                        Some(_) | None => {
+                            // Source break
+                            //
+                            // ```text
+                            // ·
+                            // ```
+                            renderer.render_snippet_break(
+                                outer_padding,
+                                self.diagnostic.severity,
+                                labeled_file.num_multi_labels,
+                                &line.multi_labels,
+                            )?;
+                        }
+                    }
+                }
+            }
+
+            // Check to see if we should render a trailing border after the
+            // final line of the snippet.
+            if labeled_files.peek().is_none() && self.diagnostic.notes.is_empty() {
+                // We don't render a border if we are at the final newline
+                // without trailing notes, because it would end up looking too
+                // spaced-out in combination with the final new line.
+            } else {
+                // Render the trailing snippet border.
+                renderer.render_snippet_empty(
+                    outer_padding,
+                    self.diagnostic.severity,
+                    labeled_file.num_multi_labels,
+                    &[],
+                )?;
+            }
+        }
+
+        // Additional notes
+        //
+        // ```text
+        // = expected type `Int`
+        //      found type `String`
+        // ```
+        for note in &self.diagnostic.notes {
+            renderer.render_snippet_note(outer_padding, note)?;
+        }
+        renderer.render_empty()
+    }
+}
+
+/// Output a short diagnostic, with a line number, severity, and message.
+pub struct ShortDiagnostic<'diagnostic, FileId> {
+    diagnostic: &'diagnostic Diagnostic<FileId>,
+    show_notes: bool,
+}
+
+impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId>
+where
+    FileId: Copy + PartialEq,
+{
+    pub fn new(
+        diagnostic: &'diagnostic Diagnostic<FileId>,
+        show_notes: bool,
+    ) -> ShortDiagnostic<'diagnostic, FileId> {
+        ShortDiagnostic {
+            diagnostic,
+            show_notes,
+        }
+    }
+
+    pub fn render<'files>(
+        &self,
+        files: &'files impl Files<'files, FileId = FileId>,
+        renderer: &mut Renderer<'_, '_>,
+    ) -> Result<(), Error>
+    where
+        FileId: 'files,
+    {
+        // Located headers
+        //
+        // ```text
+        // test:2:9: error[E0001]: unexpected type in `+` application
+        // ```
+        let mut primary_labels_encountered = 0;
+        let labels = self.diagnostic.labels.iter();
+        for label in labels.filter(|label| label.style == LabelStyle::Primary) {
+            primary_labels_encountered += 1;
+
+            renderer.render_header(
+                Some(&Locus {
+                    name: files.name(label.file_id)?.to_string(),
+                    location: files.location(label.file_id, label.range.start)?,
+                }),
+                self.diagnostic.severity,
+                self.diagnostic.code.as_deref(),
+                self.diagnostic.message.as_str(),
+            )?;
+        }
+
+        // Fallback to printing a non-located header if no primary labels were encountered
+        //
+        // ```text
+        // error[E0002]: Bad config found
+        // ```
+        if primary_labels_encountered == 0 {
+            renderer.render_header(
+                None,
+                self.diagnostic.severity,
+                self.diagnostic.code.as_deref(),
+                self.diagnostic.message.as_str(),
+            )?;
+        }
+
+        if self.show_notes {
+            // Additional notes
+            //
+            // ```text
+            // = expected type `Int`
+            //      found type `String`
+            // ```
+            for note in &self.diagnostic.notes {
+                renderer.render_snippet_note(0, note)?;
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/tests/snapshots/term__empty__medium_color.snap b/tests/snapshots/term__empty__medium_color.snap
new file mode 100644 (file)
index 0000000..e4a14fb
--- /dev/null
@@ -0,0 +1,11 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}bug{bold bright}: {/}
+{fg:Red bold bright}error{bold bright}: {/}
+{fg:Yellow bold bright}warning{bold bright}: {/}
+{fg:Green bold bright}note{bold bright}: {/}
+{fg:Cyan bold bright}help{bold bright}: {/}
+{fg:Red bold bright}bug{bold bright}: {/}
+
diff --git a/tests/snapshots/term__empty__medium_no_color.snap b/tests/snapshots/term__empty__medium_no_color.snap
new file mode 100644 (file)
index 0000000..6a8bf45
--- /dev/null
@@ -0,0 +1,11 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+bug: 
+error: 
+warning: 
+note: 
+help: 
+bug: 
+
diff --git a/tests/snapshots/term__empty__rich_ascii_no_color.snap b/tests/snapshots/term__empty__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..1ec1a94
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+bug: 
+
+error: 
+
+warning: 
+
+note: 
+
+help: 
+
+bug: 
+
+
diff --git a/tests/snapshots/term__empty__rich_color.snap b/tests/snapshots/term__empty__rich_color.snap
new file mode 100644 (file)
index 0000000..f94d46b
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}bug{bold bright}: {/}
+
+{fg:Red bold bright}error{bold bright}: {/}
+
+{fg:Yellow bold bright}warning{bold bright}: {/}
+
+{fg:Green bold bright}note{bold bright}: {/}
+
+{fg:Cyan bold bright}help{bold bright}: {/}
+
+{fg:Red bold bright}bug{bold bright}: {/}
+
+
diff --git a/tests/snapshots/term__empty__rich_no_color.snap b/tests/snapshots/term__empty__rich_no_color.snap
new file mode 100644 (file)
index 0000000..1ec1a94
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+bug: 
+
+error: 
+
+warning: 
+
+note: 
+
+help: 
+
+bug: 
+
+
diff --git a/tests/snapshots/term__empty__short_color.snap b/tests/snapshots/term__empty__short_color.snap
new file mode 100644 (file)
index 0000000..e4a14fb
--- /dev/null
@@ -0,0 +1,11 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}bug{bold bright}: {/}
+{fg:Red bold bright}error{bold bright}: {/}
+{fg:Yellow bold bright}warning{bold bright}: {/}
+{fg:Green bold bright}note{bold bright}: {/}
+{fg:Cyan bold bright}help{bold bright}: {/}
+{fg:Red bold bright}bug{bold bright}: {/}
+
diff --git a/tests/snapshots/term__empty__short_no_color.snap b/tests/snapshots/term__empty__short_no_color.snap
new file mode 100644 (file)
index 0000000..6a8bf45
--- /dev/null
@@ -0,0 +1,11 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+bug: 
+error: 
+warning: 
+note: 
+help: 
+bug: 
+
diff --git a/tests/snapshots/term__empty_ranges__medium_color.snap b/tests/snapshots/term__empty_ranges__medium_color.snap
new file mode 100644 (file)
index 0000000..3713ee4
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+hello:1:7: {fg:Green bold bright}note{bold bright}: middle{/}
+hello:1:13: {fg:Green bold bright}note{bold bright}: end of line{/}
+hello:2:11: {fg:Green bold bright}note{bold bright}: end of line{/}
+hello:3:4: {fg:Green bold bright}note{bold bright}: end of file{/}
+
diff --git a/tests/snapshots/term__empty_ranges__medium_no_color.snap b/tests/snapshots/term__empty_ranges__medium_no_color.snap
new file mode 100644 (file)
index 0000000..635a830
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+hello:1:7: note: middle
+hello:1:13: note: end of line
+hello:2:11: note: end of line
+hello:3:4: note: end of file
+
diff --git a/tests/snapshots/term__empty_ranges__rich_ascii_no_color.snap b/tests/snapshots/term__empty_ranges__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..bbf44fb
--- /dev/null
@@ -0,0 +1,29 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+note: middle
+  --> hello:1:7
+  |
+1 | Hello world!
+  |       ^ middle
+
+note: end of line
+  --> hello:1:13
+  |
+1 | Hello world!
+  |             ^ end of line
+
+note: end of line
+  --> hello:2:11
+  |
+2 | Bye world!
+  |           ^ end of line
+
+note: end of file
+  --> hello:3:4
+  |
+3 |    
+  |    ^ end of file
+
+
diff --git a/tests/snapshots/term__empty_ranges__rich_color.snap b/tests/snapshots/term__empty_ranges__rich_color.snap
new file mode 100644 (file)
index 0000000..89bb2c0
--- /dev/null
@@ -0,0 +1,29 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Green bold bright}note{bold bright}: middle{/}
+  {fg:Blue}┌─{/} hello:1:7
+  {fg:Blue}│{/}
+{fg:Blue}1{/} {fg:Blue}│{/} Hello {fg:Green}w{/}orld!
+  {fg:Blue}│{/}       {fg:Green}^{/} {fg:Green}middle{/}
+
+{fg:Green bold bright}note{bold bright}: end of line{/}
+  {fg:Blue}┌─{/} hello:1:13
+  {fg:Blue}│{/}
+{fg:Blue}1{/} {fg:Blue}│{/} Hello world!
+  {fg:Blue}│{/}             {fg:Green}^{/} {fg:Green}end of line{/}
+
+{fg:Green bold bright}note{bold bright}: end of line{/}
+  {fg:Blue}┌─{/} hello:2:11
+  {fg:Blue}│{/}
+{fg:Blue}2{/} {fg:Blue}│{/} Bye world!
+  {fg:Blue}│{/}           {fg:Green}^{/} {fg:Green}end of line{/}
+
+{fg:Green bold bright}note{bold bright}: end of file{/}
+  {fg:Blue}┌─{/} hello:3:4
+  {fg:Blue}│{/}
+{fg:Blue}3{/} {fg:Blue}│{/}    
+  {fg:Blue}│{/}    {fg:Green}^{/} {fg:Green}end of file{/}
+
+
diff --git a/tests/snapshots/term__empty_ranges__rich_no_color.snap b/tests/snapshots/term__empty_ranges__rich_no_color.snap
new file mode 100644 (file)
index 0000000..862605c
--- /dev/null
@@ -0,0 +1,29 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+note: middle
+  ┌─ hello:1:7
+  │
+1 │ Hello world!
+  │       ^ middle
+
+note: end of line
+  ┌─ hello:1:13
+  │
+1 │ Hello world!
+  │             ^ end of line
+
+note: end of line
+  ┌─ hello:2:11
+  │
+2 │ Bye world!
+  │           ^ end of line
+
+note: end of file
+  ┌─ hello:3:4
+  │
+3 │    
+  │    ^ end of file
+
+
diff --git a/tests/snapshots/term__empty_ranges__short_color.snap b/tests/snapshots/term__empty_ranges__short_color.snap
new file mode 100644 (file)
index 0000000..3713ee4
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+hello:1:7: {fg:Green bold bright}note{bold bright}: middle{/}
+hello:1:13: {fg:Green bold bright}note{bold bright}: end of line{/}
+hello:2:11: {fg:Green bold bright}note{bold bright}: end of line{/}
+hello:3:4: {fg:Green bold bright}note{bold bright}: end of file{/}
+
diff --git a/tests/snapshots/term__empty_ranges__short_no_color.snap b/tests/snapshots/term__empty_ranges__short_no_color.snap
new file mode 100644 (file)
index 0000000..635a830
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+hello:1:7: note: middle
+hello:1:13: note: end of line
+hello:2:11: note: end of line
+hello:3:4: note: end of file
+
diff --git a/tests/snapshots/term__fizz_buzz__medium_color.snap b/tests/snapshots/term__fizz_buzz__medium_color.snap
new file mode 100644 (file)
index 0000000..ef97c2c
--- /dev/null
@@ -0,0 +1,11 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+FizzBuzz.fun:8:12: {fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+ {fg:Blue}={/} expected type `String`
+      found type `Nat`
+FizzBuzz.fun:16:16: {fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+ {fg:Blue}={/} expected type `String`
+      found type `Nat`
+
diff --git a/tests/snapshots/term__fizz_buzz__medium_no_color.snap b/tests/snapshots/term__fizz_buzz__medium_no_color.snap
new file mode 100644 (file)
index 0000000..1120064
--- /dev/null
@@ -0,0 +1,11 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+FizzBuzz.fun:8:12: error[E0308]: `case` clauses have incompatible types
+ = expected type `String`
+      found type `Nat`
+FizzBuzz.fun:16:16: error[E0308]: `case` clauses have incompatible types
+ = expected type `String`
+      found type `Nat`
+
diff --git a/tests/snapshots/term__fizz_buzz__rich_ascii_no_color.snap b/tests/snapshots/term__fizz_buzz__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..55cb453
--- /dev/null
@@ -0,0 +1,42 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0308]: `case` clauses have incompatible types
+  --> FizzBuzz.fun:8:12
+  |  
+3 |   fizz₁ : Nat → String
+  |                 ------ expected type `String` found here
+4 |   fizz₁ num = case (mod num 5) (mod num 3) of
+  | /-------------'
+5 | |     0 0 => "FizzBuzz"
+6 | |     0 _ => "Fizz"
+7 | |     _ 0 => "Buzz"
+8 | |     _ _ => num
+  | |            ^^^ expected `String`, found `Nat`
+  | \--------------' `case` clauses have incompatible types
+  |  
+  = expected type `String`
+       found type `Nat`
+
+error[E0308]: `case` clauses have incompatible types
+   --> FizzBuzz.fun:16:16
+   |  
+10 |   fizz₂ : Nat → String
+   |                 ------ expected type `String` found here
+11 |   fizz₂ num =
+12 | /     case (mod num 5) (mod num 3) of
+13 | |         0 0 => "FizzBuzz"
+   | |                ---------- this is found to be of type `String`
+14 | |         0 _ => "Fizz"
+   | |                ------ this is found to be of type `String`
+15 | |         _ 0 => "Buzz"
+   | |                ------ this is found to be of type `String`
+16 | |         _ _ => num
+   | |                ^^^ expected `String`, found `Nat`
+   | \------------------' `case` clauses have incompatible types
+   |  
+   = expected type `String`
+        found type `Nat`
+
+
diff --git a/tests/snapshots/term__fizz_buzz__rich_color.snap b/tests/snapshots/term__fizz_buzz__rich_color.snap
new file mode 100644 (file)
index 0000000..79dded2
--- /dev/null
@@ -0,0 +1,42 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+  {fg:Blue}┌─{/} FizzBuzz.fun:8:12
+  {fg:Blue}│{/}  
+{fg:Blue}3{/} {fg:Blue}│{/}   fizz₁ : Nat → String
+  {fg:Blue}│{/}                 {fg:Blue}------{/} {fg:Blue}expected type `String` found here{/}
+{fg:Blue}4{/} {fg:Blue}│{/}   fizz₁ num = case (mod num 5) (mod num 3) of
+  {fg:Blue}│{/} {fg:Blue}╭{/}{fg:Blue}─────────────'{/}
+{fg:Blue}5{/} {fg:Blue}│{/} {fg:Blue}│{/}     0 0 => "FizzBuzz"
+{fg:Blue}6{/} {fg:Blue}│{/} {fg:Blue}│{/}     0 _ => "Fizz"
+{fg:Blue}7{/} {fg:Blue}│{/} {fg:Blue}│{/}     _ 0 => "Buzz"
+{fg:Blue}8{/} {fg:Blue}│{/} {fg:Blue}│{/}     _ _ => {fg:Red}num{/}
+  {fg:Blue}│{/} {fg:Blue}│{/}            {fg:Red}^^^{/} {fg:Red}expected `String`, found `Nat`{/}
+  {fg:Blue}│{/} {fg:Blue}╰{/}{fg:Blue}──────────────' `case` clauses have incompatible types{/}
+  {fg:Blue}│{/}  
+  {fg:Blue}={/} expected type `String`
+       found type `Nat`
+
+{fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+   {fg:Blue}┌─{/} FizzBuzz.fun:16:16
+   {fg:Blue}│{/}  
+{fg:Blue}10{/} {fg:Blue}│{/}   fizz₂ : Nat → String
+   {fg:Blue}│{/}                 {fg:Blue}------{/} {fg:Blue}expected type `String` found here{/}
+{fg:Blue}11{/} {fg:Blue}│{/}   fizz₂ num =
+{fg:Blue}12{/} {fg:Blue}│{/} {fg:Blue}╭{/}     case (mod num 5) (mod num 3) of
+{fg:Blue}13{/} {fg:Blue}│{/} {fg:Blue}│{/}         0 0 => "FizzBuzz"
+   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}----------{/} {fg:Blue}this is found to be of type `String`{/}
+{fg:Blue}14{/} {fg:Blue}│{/} {fg:Blue}│{/}         0 _ => "Fizz"
+   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}------{/} {fg:Blue}this is found to be of type `String`{/}
+{fg:Blue}15{/} {fg:Blue}│{/} {fg:Blue}│{/}         _ 0 => "Buzz"
+   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}------{/} {fg:Blue}this is found to be of type `String`{/}
+{fg:Blue}16{/} {fg:Blue}│{/} {fg:Blue}│{/}         _ _ => {fg:Red}num{/}
+   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Red}^^^{/} {fg:Red}expected `String`, found `Nat`{/}
+   {fg:Blue}│{/} {fg:Blue}╰{/}{fg:Blue}──────────────────' `case` clauses have incompatible types{/}
+   {fg:Blue}│{/}  
+   {fg:Blue}={/} expected type `String`
+        found type `Nat`
+
+
diff --git a/tests/snapshots/term__fizz_buzz__rich_no_color.snap b/tests/snapshots/term__fizz_buzz__rich_no_color.snap
new file mode 100644 (file)
index 0000000..a0b7260
--- /dev/null
@@ -0,0 +1,42 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0308]: `case` clauses have incompatible types
+  ┌─ FizzBuzz.fun:8:12
+  │  
+3 │   fizz₁ : Nat → String
+  │                 ------ expected type `String` found here
+4 │   fizz₁ num = case (mod num 5) (mod num 3) of
+  │ ╭─────────────'
+5 │ │     0 0 => "FizzBuzz"
+6 │ │     0 _ => "Fizz"
+7 │ │     _ 0 => "Buzz"
+8 │ │     _ _ => num
+  │ │            ^^^ expected `String`, found `Nat`
+  │ ╰──────────────' `case` clauses have incompatible types
+  │  
+  = expected type `String`
+       found type `Nat`
+
+error[E0308]: `case` clauses have incompatible types
+   ┌─ FizzBuzz.fun:16:16
+   │  
+10 │   fizz₂ : Nat → String
+   │                 ------ expected type `String` found here
+11 │   fizz₂ num =
+12 │ ╭     case (mod num 5) (mod num 3) of
+13 │ │         0 0 => "FizzBuzz"
+   │ │                ---------- this is found to be of type `String`
+14 │ │         0 _ => "Fizz"
+   │ │                ------ this is found to be of type `String`
+15 │ │         _ 0 => "Buzz"
+   │ │                ------ this is found to be of type `String`
+16 │ │         _ _ => num
+   │ │                ^^^ expected `String`, found `Nat`
+   │ ╰──────────────────' `case` clauses have incompatible types
+   │  
+   = expected type `String`
+        found type `Nat`
+
+
diff --git a/tests/snapshots/term__fizz_buzz__short_color.snap b/tests/snapshots/term__fizz_buzz__short_color.snap
new file mode 100644 (file)
index 0000000..d25ba9d
--- /dev/null
@@ -0,0 +1,7 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+FizzBuzz.fun:8:12: {fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+FizzBuzz.fun:16:16: {fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+
diff --git a/tests/snapshots/term__fizz_buzz__short_no_color.snap b/tests/snapshots/term__fizz_buzz__short_no_color.snap
new file mode 100644 (file)
index 0000000..00a243f
--- /dev/null
@@ -0,0 +1,7 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+FizzBuzz.fun:8:12: error[E0308]: `case` clauses have incompatible types
+FizzBuzz.fun:16:16: error[E0308]: `case` clauses have incompatible types
+
diff --git a/tests/snapshots/term__message__medium_color.snap b/tests/snapshots/term__message__medium_color.snap
new file mode 100644 (file)
index 0000000..f0b16d8
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error{bold bright}: a message{/}
+{fg:Yellow bold bright}warning{bold bright}: a message{/}
+{fg:Green bold bright}note{bold bright}: a message{/}
+{fg:Cyan bold bright}help{bold bright}: a message{/}
+
diff --git a/tests/snapshots/term__message__medium_no_color.snap b/tests/snapshots/term__message__medium_no_color.snap
new file mode 100644 (file)
index 0000000..43565ac
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: a message
+warning: a message
+note: a message
+help: a message
+
diff --git a/tests/snapshots/term__message__rich_ascii_no_color.snap b/tests/snapshots/term__message__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..d3abe84
--- /dev/null
@@ -0,0 +1,13 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: a message
+
+warning: a message
+
+note: a message
+
+help: a message
+
+
diff --git a/tests/snapshots/term__message__rich_color.snap b/tests/snapshots/term__message__rich_color.snap
new file mode 100644 (file)
index 0000000..c1bd423
--- /dev/null
@@ -0,0 +1,13 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error{bold bright}: a message{/}
+
+{fg:Yellow bold bright}warning{bold bright}: a message{/}
+
+{fg:Green bold bright}note{bold bright}: a message{/}
+
+{fg:Cyan bold bright}help{bold bright}: a message{/}
+
+
diff --git a/tests/snapshots/term__message__rich_no_color.snap b/tests/snapshots/term__message__rich_no_color.snap
new file mode 100644 (file)
index 0000000..d3abe84
--- /dev/null
@@ -0,0 +1,13 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: a message
+
+warning: a message
+
+note: a message
+
+help: a message
+
+
diff --git a/tests/snapshots/term__message__short_color.snap b/tests/snapshots/term__message__short_color.snap
new file mode 100644 (file)
index 0000000..f0b16d8
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error{bold bright}: a message{/}
+{fg:Yellow bold bright}warning{bold bright}: a message{/}
+{fg:Green bold bright}note{bold bright}: a message{/}
+{fg:Cyan bold bright}help{bold bright}: a message{/}
+
diff --git a/tests/snapshots/term__message__short_no_color.snap b/tests/snapshots/term__message__short_no_color.snap
new file mode 100644 (file)
index 0000000..43565ac
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: a message
+warning: a message
+note: a message
+help: a message
+
diff --git a/tests/snapshots/term__message_and_notes__medium_color.snap b/tests/snapshots/term__message_and_notes__medium_color.snap
new file mode 100644 (file)
index 0000000..f44b2a4
--- /dev/null
@@ -0,0 +1,13 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error{bold bright}: a message{/}
+ {fg:Blue}={/} a note
+{fg:Yellow bold bright}warning{bold bright}: a message{/}
+ {fg:Blue}={/} a note
+{fg:Green bold bright}note{bold bright}: a message{/}
+ {fg:Blue}={/} a note
+{fg:Cyan bold bright}help{bold bright}: a message{/}
+ {fg:Blue}={/} a note
+
diff --git a/tests/snapshots/term__message_and_notes__medium_no_color.snap b/tests/snapshots/term__message_and_notes__medium_no_color.snap
new file mode 100644 (file)
index 0000000..bab7a65
--- /dev/null
@@ -0,0 +1,13 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: a message
+ = a note
+warning: a message
+ = a note
+note: a message
+ = a note
+help: a message
+ = a note
+
diff --git a/tests/snapshots/term__message_and_notes__rich_ascii_no_color.snap b/tests/snapshots/term__message_and_notes__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..d4e209b
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: a message
+ = a note
+
+warning: a message
+ = a note
+
+note: a message
+ = a note
+
+help: a message
+ = a note
+
+
diff --git a/tests/snapshots/term__message_and_notes__rich_color.snap b/tests/snapshots/term__message_and_notes__rich_color.snap
new file mode 100644 (file)
index 0000000..56900e8
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error{bold bright}: a message{/}
+ {fg:Blue}={/} a note
+
+{fg:Yellow bold bright}warning{bold bright}: a message{/}
+ {fg:Blue}={/} a note
+
+{fg:Green bold bright}note{bold bright}: a message{/}
+ {fg:Blue}={/} a note
+
+{fg:Cyan bold bright}help{bold bright}: a message{/}
+ {fg:Blue}={/} a note
+
+
diff --git a/tests/snapshots/term__message_and_notes__rich_no_color.snap b/tests/snapshots/term__message_and_notes__rich_no_color.snap
new file mode 100644 (file)
index 0000000..d4e209b
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: a message
+ = a note
+
+warning: a message
+ = a note
+
+note: a message
+ = a note
+
+help: a message
+ = a note
+
+
diff --git a/tests/snapshots/term__message_and_notes__short_color.snap b/tests/snapshots/term__message_and_notes__short_color.snap
new file mode 100644 (file)
index 0000000..f0b16d8
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error{bold bright}: a message{/}
+{fg:Yellow bold bright}warning{bold bright}: a message{/}
+{fg:Green bold bright}note{bold bright}: a message{/}
+{fg:Cyan bold bright}help{bold bright}: a message{/}
+
diff --git a/tests/snapshots/term__message_and_notes__short_no_color.snap b/tests/snapshots/term__message_and_notes__short_no_color.snap
new file mode 100644 (file)
index 0000000..43565ac
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: a message
+warning: a message
+note: a message
+help: a message
+
diff --git a/tests/snapshots/term__message_errorcode__rich_ascii_no_color.snap b/tests/snapshots/term__message_errorcode__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..e82fb7c
--- /dev/null
@@ -0,0 +1,21 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0001]: a message
+
+warning[W001]: a message
+
+note[N0815]: a message
+
+help[H4711]: a message
+
+error: where did my errorcode go?
+
+warning: where did my errorcode go?
+
+note: where did my errorcode go?
+
+help: where did my errorcode go?
+
+
diff --git a/tests/snapshots/term__message_errorcode__rich_no_color.snap b/tests/snapshots/term__message_errorcode__rich_no_color.snap
new file mode 100644 (file)
index 0000000..e82fb7c
--- /dev/null
@@ -0,0 +1,21 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0001]: a message
+
+warning[W001]: a message
+
+note[N0815]: a message
+
+help[H4711]: a message
+
+error: where did my errorcode go?
+
+warning: where did my errorcode go?
+
+note: where did my errorcode go?
+
+help: where did my errorcode go?
+
+
diff --git a/tests/snapshots/term__message_errorcode__short_no_color.snap b/tests/snapshots/term__message_errorcode__short_no_color.snap
new file mode 100644 (file)
index 0000000..ccddd8d
--- /dev/null
@@ -0,0 +1,13 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0001]: a message
+warning[W001]: a message
+note[N0815]: a message
+help[H4711]: a message
+error: where did my errorcode go?
+warning: where did my errorcode go?
+note: where did my errorcode go?
+help: where did my errorcode go?
+
diff --git a/tests/snapshots/term__multifile__medium_color.snap b/tests/snapshots/term__multifile__medium_color.snap
new file mode 100644 (file)
index 0000000..6c03d83
--- /dev/null
@@ -0,0 +1,12 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+Data/Nat.fun:7:13: {fg:Red bold bright}error{bold bright}: unknown builtin: `NATRAL`{/}
+ {fg:Blue}={/} there is a builtin with a similar name: `NATURAL`
+Data/Nat.fun:17:16: {fg:Yellow bold bright}warning{bold bright}: unused parameter pattern: `n₂`{/}
+ {fg:Blue}={/} consider using a wildcard pattern: `_`
+Test.fun:4:11: {fg:Red bold bright}error[E0001]{bold bright}: unexpected type in application of `_+_`{/}
+ {fg:Blue}={/} expected type `Nat`
+      found type `String`
+
diff --git a/tests/snapshots/term__multifile__medium_no_color.snap b/tests/snapshots/term__multifile__medium_no_color.snap
new file mode 100644 (file)
index 0000000..db33fc0
--- /dev/null
@@ -0,0 +1,12 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+Data/Nat.fun:7:13: error: unknown builtin: `NATRAL`
+ = there is a builtin with a similar name: `NATURAL`
+Data/Nat.fun:17:16: warning: unused parameter pattern: `n₂`
+ = consider using a wildcard pattern: `_`
+Test.fun:4:11: error[E0001]: unexpected type in application of `_+_`
+ = expected type `Nat`
+      found type `String`
+
diff --git a/tests/snapshots/term__multifile__rich_ascii_no_color.snap b/tests/snapshots/term__multifile__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..e162ed2
--- /dev/null
@@ -0,0 +1,35 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: unknown builtin: `NATRAL`
+  --> Data/Nat.fun:7:13
+  |
+7 | {-# BUILTIN NATRAL Nat #-}
+  |             ^^^^^^ unknown builtin
+  |
+  = there is a builtin with a similar name: `NATURAL`
+
+warning: unused parameter pattern: `n₂`
+   --> Data/Nat.fun:17:16
+   |
+17 | zero    - succ n₂ = zero
+   |                ^^ unused parameter
+   |
+   = consider using a wildcard pattern: `_`
+
+error[E0001]: unexpected type in application of `_+_`
+   --> Test.fun:4:11
+   |
+ 4 | _ = 123 + "hello"
+   |           ^^^^^^^ expected `Nat`, found `String`
+   |
+   --> Data/Nat.fun:11:1
+   |
+11 | _+_ : Nat → Nat → Nat
+   | --------------------- based on the definition of `_+_`
+   |
+   = expected type `Nat`
+        found type `String`
+
+
diff --git a/tests/snapshots/term__multifile__rich_color.snap b/tests/snapshots/term__multifile__rich_color.snap
new file mode 100644 (file)
index 0000000..514be49
--- /dev/null
@@ -0,0 +1,35 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error{bold bright}: unknown builtin: `NATRAL`{/}
+  {fg:Blue}┌─{/} Data/Nat.fun:7:13
+  {fg:Blue}│{/}
+{fg:Blue}7{/} {fg:Blue}│{/} {-# BUILTIN {fg:Red}NATRAL{/} Nat #-}
+  {fg:Blue}│{/}             {fg:Red}^^^^^^{/} {fg:Red}unknown builtin{/}
+  {fg:Blue}│{/}
+  {fg:Blue}={/} there is a builtin with a similar name: `NATURAL`
+
+{fg:Yellow bold bright}warning{bold bright}: unused parameter pattern: `n₂`{/}
+   {fg:Blue}┌─{/} Data/Nat.fun:17:16
+   {fg:Blue}│{/}
+{fg:Blue}17{/} {fg:Blue}│{/} zero    - succ {fg:Yellow}n₂{/} = zero
+   {fg:Blue}│{/}                {fg:Yellow}^^{/} {fg:Yellow}unused parameter{/}
+   {fg:Blue}│{/}
+   {fg:Blue}={/} consider using a wildcard pattern: `_`
+
+{fg:Red bold bright}error[E0001]{bold bright}: unexpected type in application of `_+_`{/}
+   {fg:Blue}┌─{/} Test.fun:4:11
+   {fg:Blue}│{/}
+{fg:Blue} 4{/} {fg:Blue}│{/} _ = 123 + {fg:Red}"hello"{/}
+   {fg:Blue}│{/}           {fg:Red}^^^^^^^{/} {fg:Red}expected `Nat`, found `String`{/}
+   {fg:Blue}│{/}
+   {fg:Blue}┌─{/} Data/Nat.fun:11:1
+   {fg:Blue}│{/}
+{fg:Blue}11{/} {fg:Blue}│{/} _+_ : Nat → Nat → Nat
+   {fg:Blue}│{/} {fg:Blue}---------------------{/} {fg:Blue}based on the definition of `_+_`{/}
+   {fg:Blue}│{/}
+   {fg:Blue}={/} expected type `Nat`
+        found type `String`
+
+
diff --git a/tests/snapshots/term__multifile__rich_no_color.snap b/tests/snapshots/term__multifile__rich_no_color.snap
new file mode 100644 (file)
index 0000000..a626b54
--- /dev/null
@@ -0,0 +1,35 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: unknown builtin: `NATRAL`
+  ┌─ Data/Nat.fun:7:13
+  │
+7 │ {-# BUILTIN NATRAL Nat #-}
+  │             ^^^^^^ unknown builtin
+  │
+  = there is a builtin with a similar name: `NATURAL`
+
+warning: unused parameter pattern: `n₂`
+   ┌─ Data/Nat.fun:17:16
+   │
+17 │ zero    - succ n₂ = zero
+   │                ^^ unused parameter
+   │
+   = consider using a wildcard pattern: `_`
+
+error[E0001]: unexpected type in application of `_+_`
+   ┌─ Test.fun:4:11
+   │
+ 4 │ _ = 123 + "hello"
+   │           ^^^^^^^ expected `Nat`, found `String`
+   │
+   ┌─ Data/Nat.fun:11:1
+   │
+11 │ _+_ : Nat → Nat → Nat
+   │ --------------------- based on the definition of `_+_`
+   │
+   = expected type `Nat`
+        found type `String`
+
+
diff --git a/tests/snapshots/term__multifile__short_color.snap b/tests/snapshots/term__multifile__short_color.snap
new file mode 100644 (file)
index 0000000..e9f4066
--- /dev/null
@@ -0,0 +1,8 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+Data/Nat.fun:7:13: {fg:Red bold bright}error{bold bright}: unknown builtin: `NATRAL`{/}
+Data/Nat.fun:17:16: {fg:Yellow bold bright}warning{bold bright}: unused parameter pattern: `n₂`{/}
+Test.fun:4:11: {fg:Red bold bright}error[E0001]{bold bright}: unexpected type in application of `_+_`{/}
+
diff --git a/tests/snapshots/term__multifile__short_no_color.snap b/tests/snapshots/term__multifile__short_no_color.snap
new file mode 100644 (file)
index 0000000..e3e020e
--- /dev/null
@@ -0,0 +1,8 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+Data/Nat.fun:7:13: error: unknown builtin: `NATRAL`
+Data/Nat.fun:17:16: warning: unused parameter pattern: `n₂`
+Test.fun:4:11: error[E0001]: unexpected type in application of `_+_`
+
diff --git a/tests/snapshots/term__multiline_omit__rich_no_color.snap b/tests/snapshots/term__multiline_omit__rich_no_color.snap
new file mode 100644 (file)
index 0000000..bb9363d
--- /dev/null
@@ -0,0 +1,38 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[empty_if]: empty elseif block
+   ┌─ empty_if_comments.lua:1:1
+   │    
+ 1 │ ╭   elseif 3 then
+ 2 │ │   
+ 3 │ │ ╭ 
+ 4 │ │ │ 
+ 5 │ │ │ 
+   · │ │
+ 8 │ │ │ 
+ 9 │ │ │ 
+   │ │ ╰' content should be in here
+10 │ │   else
+   │ ╰───^
+
+error[E0308]: mismatched types
+   ┌─ src/lib.rs:2:6
+   │  
+ 2 │       1
+   │ ╭─────^
+ 3 │ │     + 1
+ 4 │ │     + 1
+   · │
+ 7 │ │     +1
+   │ │      - missing whitespace
+ 8 │ │     + 1
+ 9 │ │     + 1
+10 │ │     + 1
+   │ ╰───────^ expected (), found integer
+   │  
+   = note:     expected type `()`
+       found type `{integer}`
+
+
diff --git a/tests/snapshots/term__multiline_overlapping__medium_color.snap b/tests/snapshots/term__multiline_overlapping__medium_color.snap
new file mode 100644 (file)
index 0000000..afddf24
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+codespan/src/file.rs:4:34: {fg:Red bold bright}error[E0308]{bold bright}: match arms have incompatible types{/}
+ {fg:Blue}={/} expected type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+      found type `LineIndexOutOfBoundsError`
+
+
diff --git a/tests/snapshots/term__multiline_overlapping__medium_no_color.snap b/tests/snapshots/term__multiline_overlapping__medium_no_color.snap
new file mode 100644 (file)
index 0000000..0e50bd8
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+codespan/src/file.rs:4:34: error[E0308]: match arms have incompatible types
+ = expected type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+      found type `LineIndexOutOfBoundsError`
+
+
diff --git a/tests/snapshots/term__multiline_overlapping__rich_ascii_no_color.snap b/tests/snapshots/term__multiline_overlapping__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..3623d4d
--- /dev/null
@@ -0,0 +1,25 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0308]: match arms have incompatible types
+  --> codespan/src/file.rs:4:34
+  |    
+1 |   /         match line_index.compare(self.last_line_index()) {
+2 |   |             Ordering::Less => Ok(self.line_starts()[line_index.to_usize()]),
+  |   |                               --------------------------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+3 |   |             Ordering::Equal => Ok(self.source_span().end()),
+  |   |                                ---------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+4 |   |             Ordering::Greater => LineIndexOutOfBoundsError {
+  | /-|----------------------------------^
+5 | | |                 given: line_index,
+6 | | |                 max: self.last_line_index(),
+7 | | |             },
+  | \-|-------------^ expected enum `Result`, found struct `LineIndexOutOfBoundsError`
+8 |   |         }
+  |   \---------' `match` arms have incompatible types
+  |    
+  = expected type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+       found type `LineIndexOutOfBoundsError`
+
+
diff --git a/tests/snapshots/term__multiline_overlapping__rich_color.snap b/tests/snapshots/term__multiline_overlapping__rich_color.snap
new file mode 100644 (file)
index 0000000..10701fb
--- /dev/null
@@ -0,0 +1,25 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error[E0308]{bold bright}: match arms have incompatible types{/}
+  {fg:Blue}┌─{/} codespan/src/file.rs:4:34
+  {fg:Blue}│{/}    
+{fg:Blue}1{/} {fg:Blue}│{/}   {fg:Blue}╭{/}         match line_index.compare(self.last_line_index()) {
+{fg:Blue}2{/} {fg:Blue}│{/}   {fg:Blue}│{/}             Ordering::Less => Ok(self.line_starts()[line_index.to_usize()]),
+  {fg:Blue}│{/}   {fg:Blue}│{/}                               {fg:Blue}---------------------------------------------{/} {fg:Blue}this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`{/}
+{fg:Blue}3{/} {fg:Blue}│{/}   {fg:Blue}│{/}             Ordering::Equal => Ok(self.source_span().end()),
+  {fg:Blue}│{/}   {fg:Blue}│{/}                                {fg:Blue}----------------------------{/} {fg:Blue}this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`{/}
+{fg:Blue}4{/} {fg:Blue}│{/}   {fg:Blue}│{/}             Ordering::Greater => {fg:Red}LineIndexOutOfBoundsError {{/}
+  {fg:Blue}│{/} {fg:Red}╭{/}{fg:Red}─{/}{fg:Blue}│{/}{fg:Red}──────────────────────────────────^{/}
+{fg:Blue}5{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} {fg:Red}                given: line_index,{/}
+{fg:Blue}6{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} {fg:Red}                max: self.last_line_index(),{/}
+{fg:Blue}7{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} {fg:Red}            }{/},
+  {fg:Blue}│{/} {fg:Red}╰{/}{fg:Red}─{/}{fg:Blue}│{/}{fg:Red}─────────────^ expected enum `Result`, found struct `LineIndexOutOfBoundsError`{/}
+{fg:Blue}8{/} {fg:Blue}│{/}   {fg:Blue}│{/}         }
+  {fg:Blue}│{/}   {fg:Blue}╰{/}{fg:Blue}─────────' `match` arms have incompatible types{/}
+  {fg:Blue}│{/}    
+  {fg:Blue}={/} expected type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+       found type `LineIndexOutOfBoundsError`
+
+
diff --git a/tests/snapshots/term__multiline_overlapping__rich_no_color.snap b/tests/snapshots/term__multiline_overlapping__rich_no_color.snap
new file mode 100644 (file)
index 0000000..f6802c8
--- /dev/null
@@ -0,0 +1,25 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0308]: match arms have incompatible types
+  ┌─ codespan/src/file.rs:4:34
+  │    
+1 │   ╭         match line_index.compare(self.last_line_index()) {
+2 │   │             Ordering::Less => Ok(self.line_starts()[line_index.to_usize()]),
+  │   │                               --------------------------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+3 │   │             Ordering::Equal => Ok(self.source_span().end()),
+  │   │                                ---------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+4 │   │             Ordering::Greater => LineIndexOutOfBoundsError {
+  │ ╭─│──────────────────────────────────^
+5 │ │ │                 given: line_index,
+6 │ │ │                 max: self.last_line_index(),
+7 │ │ │             },
+  │ ╰─│─────────────^ expected enum `Result`, found struct `LineIndexOutOfBoundsError`
+8 │   │         }
+  │   ╰─────────' `match` arms have incompatible types
+  │    
+  = expected type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+       found type `LineIndexOutOfBoundsError`
+
+
diff --git a/tests/snapshots/term__multiline_overlapping__short_color.snap b/tests/snapshots/term__multiline_overlapping__short_color.snap
new file mode 100644 (file)
index 0000000..808c6d0
--- /dev/null
@@ -0,0 +1,6 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+codespan/src/file.rs:4:34: {fg:Red bold bright}error[E0308]{bold bright}: match arms have incompatible types{/}
+
diff --git a/tests/snapshots/term__multiline_overlapping__short_no_color.snap b/tests/snapshots/term__multiline_overlapping__short_no_color.snap
new file mode 100644 (file)
index 0000000..0254b6e
--- /dev/null
@@ -0,0 +1,6 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+codespan/src/file.rs:4:34: error[E0308]: match arms have incompatible types
+
diff --git a/tests/snapshots/term__overlapping__medium_color.snap b/tests/snapshots/term__overlapping__medium_color.snap
new file mode 100644 (file)
index 0000000..9ccf3e5
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+nested_impl_trait.rs:5:56: {fg:Red bold bright}error[E0666]{bold bright}: nested `impl Trait` is not allowed{/}
+typeck_type_placeholder_item.rs:1:18: {fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
+typeck_type_placeholder_item.rs:2:25: {fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
+typeck_type_placeholder_item.rs:2:28: {fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
+no_send_res_ports.rs:25:5: {fg:Red bold bright}error[E0277]{bold bright}: `std::rc::Rc<()>` cannot be sent between threads safely{/}
+ {fg:Blue}={/} help: within `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<()>`
+ {fg:Blue}={/} note: required because it appears within the type `Port<()>`
+ {fg:Blue}={/} note: required because it appears within the type `main::Foo`
+ {fg:Blue}={/} note: required because it appears within the type `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`
+{fg:Red bold bright}error{bold bright}: aborting due 5 previous errors{/}
+ {fg:Blue}={/} Some errors have detailed explanations: E0121, E0277, E0666.
+ {fg:Blue}={/} For more information about an error, try `rustc --explain E0121`.
+
diff --git a/tests/snapshots/term__overlapping__medium_no_color.snap b/tests/snapshots/term__overlapping__medium_no_color.snap
new file mode 100644 (file)
index 0000000..3b9eac9
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+nested_impl_trait.rs:5:56: error[E0666]: nested `impl Trait` is not allowed
+typeck_type_placeholder_item.rs:1:18: error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+typeck_type_placeholder_item.rs:2:25: error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+typeck_type_placeholder_item.rs:2:28: error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+no_send_res_ports.rs:25:5: error[E0277]: `std::rc::Rc<()>` cannot be sent between threads safely
+ = help: within `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<()>`
+ = note: required because it appears within the type `Port<()>`
+ = note: required because it appears within the type `main::Foo`
+ = note: required because it appears within the type `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`
+error: aborting due 5 previous errors
+ = Some errors have detailed explanations: E0121, E0277, E0666.
+ = For more information about an error, try `rustc --explain E0121`.
+
diff --git a/tests/snapshots/term__overlapping__rich_ascii_no_color.snap b/tests/snapshots/term__overlapping__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..b6002fc
--- /dev/null
@@ -0,0 +1,58 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0666]: nested `impl Trait` is not allowed
+  --> nested_impl_trait.rs:5:56
+  |
+5 | fn bad_in_ret_position(x: impl Into<u32>) -> impl Into<impl Debug> { x }
+  |                                              ----------^^^^^^^^^^-
+  |                                              |         |
+  |                                              |         nested `impl Trait` here
+  |                                              outer `impl Trait`
+
+error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+  --> typeck_type_placeholder_item.rs:1:18
+  |
+1 | fn fn_test1() -> _ { 5 }
+  |                  ^
+  |                  |
+  |                  not allowed in type signatures
+  |                  help: replace with the correct return type: `i32`
+
+error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+  --> typeck_type_placeholder_item.rs:2:25
+  |
+2 | fn fn_test2(x: i32) -> (_, _) { (x, x) }
+  |                        -^--^-
+  |                        ||  |
+  |                        ||  not allowed in type signatures
+  |                        |not allowed in type signatures
+  |                        help: replace with the correct return type: `(i32, i32)`
+
+error[E0277]: `std::rc::Rc<()>` cannot be sent between threads safely
+   --> no_send_res_ports.rs:25:5
+   |  
+25 |       thread::spawn(move|| {
+   |       ^^^^^^^^^^^^^ `std::rc::Rc<()>` cannot be sent between threads safely
+   | /-------------------'
+26 | |         let y = x;
+27 | |         println!("{:?}", y);
+28 | |     });
+   | \------' within this `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`
+   |  
+   --> libstd/thread/mod.rs:5:8
+   |
+ 5 |     F: Send + 'static,
+   |        ---- required by this bound in `std::thread::spawn`
+   |
+   = help: within `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<()>`
+   = note: required because it appears within the type `Port<()>`
+   = note: required because it appears within the type `main::Foo`
+   = note: required because it appears within the type `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`
+
+error: aborting due 5 previous errors
+ = Some errors have detailed explanations: E0121, E0277, E0666.
+ = For more information about an error, try `rustc --explain E0121`.
+
+
diff --git a/tests/snapshots/term__overlapping__rich_color.snap b/tests/snapshots/term__overlapping__rich_color.snap
new file mode 100644 (file)
index 0000000..d29a429
--- /dev/null
@@ -0,0 +1,58 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error[E0666]{bold bright}: nested `impl Trait` is not allowed{/}
+  {fg:Blue}┌─{/} nested_impl_trait.rs:5:56
+  {fg:Blue}│{/}
+{fg:Blue}5{/} {fg:Blue}│{/} fn bad_in_ret_position(x: impl Into<u32>) -> impl Into<{fg:Red}impl Debug{/}> { x }
+  {fg:Blue}│{/}                                              {fg:Blue}----------{fg:Red}^^^^^^^^^^{fg:Blue}-{/}
+  {fg:Blue}│{/}                                              {fg:Blue}│{/}         {fg:Red}│{/}
+  {fg:Blue}│{/}                                              {fg:Blue}│{/}         {fg:Red}nested `impl Trait` here{/}
+  {fg:Blue}│{/}                                              {fg:Blue}outer `impl Trait`{/}
+
+{fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
+  {fg:Blue}┌─{/} typeck_type_placeholder_item.rs:1:18
+  {fg:Blue}│{/}
+{fg:Blue}1{/} {fg:Blue}│{/} fn fn_test1() -> {fg:Red}_{/} { 5 }
+  {fg:Blue}│{/}                  {fg:Red}^{/}
+  {fg:Blue}│{/}                  {fg:Red}│{/}
+  {fg:Blue}│{/}                  {fg:Red}not allowed in type signatures{/}
+  {fg:Blue}│{/}                  {fg:Blue}help: replace with the correct return type: `i32`{/}
+
+{fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
+  {fg:Blue}┌─{/} typeck_type_placeholder_item.rs:2:25
+  {fg:Blue}│{/}
+{fg:Blue}2{/} {fg:Blue}│{/} fn fn_test2(x: i32) -> ({fg:Red}_{/}, {fg:Red}_{/}) { (x, x) }
+  {fg:Blue}│{/}                        {fg:Blue}-{fg:Red}^{fg:Blue}--{fg:Red}^{fg:Blue}-{/}
+  {fg:Blue}│{/}                        {fg:Blue}│{/}{fg:Red}│{/}  {fg:Red}│{/}
+  {fg:Blue}│{/}                        {fg:Blue}│{/}{fg:Red}│{/}  {fg:Red}not allowed in type signatures{/}
+  {fg:Blue}│{/}                        {fg:Blue}│{/}{fg:Red}not allowed in type signatures{/}
+  {fg:Blue}│{/}                        {fg:Blue}help: replace with the correct return type: `(i32, i32)`{/}
+
+{fg:Red bold bright}error[E0277]{bold bright}: `std::rc::Rc<()>` cannot be sent between threads safely{/}
+   {fg:Blue}┌─{/} no_send_res_ports.rs:25:5
+   {fg:Blue}│{/}  
+{fg:Blue}25{/} {fg:Blue}│{/}       {fg:Red}thread::spawn{/}(move|| {
+   {fg:Blue}│{/}       {fg:Red}^^^^^^^^^^^^^{/} {fg:Red}`std::rc::Rc<()>` cannot be sent between threads safely{/}
+   {fg:Blue}│{/} {fg:Blue}╭{/}{fg:Blue}───────────────────'{/}
+{fg:Blue}26{/} {fg:Blue}│{/} {fg:Blue}│{/}         let y = x;
+{fg:Blue}27{/} {fg:Blue}│{/} {fg:Blue}│{/}         println!("{:?}", y);
+{fg:Blue}28{/} {fg:Blue}│{/} {fg:Blue}│{/}     });
+   {fg:Blue}│{/} {fg:Blue}╰{/}{fg:Blue}──────' within this `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`{/}
+   {fg:Blue}│{/}  
+   {fg:Blue}┌─{/} libstd/thread/mod.rs:5:8
+   {fg:Blue}│{/}
+{fg:Blue} 5{/} {fg:Blue}│{/}     F: Send + 'static,
+   {fg:Blue}│{/}        {fg:Blue}----{/} {fg:Blue}required by this bound in `std::thread::spawn`{/}
+   {fg:Blue}│{/}
+   {fg:Blue}={/} help: within `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<()>`
+   {fg:Blue}={/} note: required because it appears within the type `Port<()>`
+   {fg:Blue}={/} note: required because it appears within the type `main::Foo`
+   {fg:Blue}={/} note: required because it appears within the type `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`
+
+{fg:Red bold bright}error{bold bright}: aborting due 5 previous errors{/}
+ {fg:Blue}={/} Some errors have detailed explanations: E0121, E0277, E0666.
+ {fg:Blue}={/} For more information about an error, try `rustc --explain E0121`.
+
+
diff --git a/tests/snapshots/term__overlapping__rich_no_color.snap b/tests/snapshots/term__overlapping__rich_no_color.snap
new file mode 100644 (file)
index 0000000..32f8eec
--- /dev/null
@@ -0,0 +1,58 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0666]: nested `impl Trait` is not allowed
+  ┌─ nested_impl_trait.rs:5:56
+  │
+5 │ fn bad_in_ret_position(x: impl Into<u32>) -> impl Into<impl Debug> { x }
+  │                                              ----------^^^^^^^^^^-
+  │                                              │         │
+  │                                              │         nested `impl Trait` here
+  │                                              outer `impl Trait`
+
+error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+  ┌─ typeck_type_placeholder_item.rs:1:18
+  │
+1 │ fn fn_test1() -> _ { 5 }
+  │                  ^
+  │                  │
+  │                  not allowed in type signatures
+  │                  help: replace with the correct return type: `i32`
+
+error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+  ┌─ typeck_type_placeholder_item.rs:2:25
+  │
+2 │ fn fn_test2(x: i32) -> (_, _) { (x, x) }
+  │                        -^--^-
+  │                        ││  │
+  │                        ││  not allowed in type signatures
+  │                        │not allowed in type signatures
+  │                        help: replace with the correct return type: `(i32, i32)`
+
+error[E0277]: `std::rc::Rc<()>` cannot be sent between threads safely
+   ┌─ no_send_res_ports.rs:25:5
+   │  
+25 │       thread::spawn(move|| {
+   │       ^^^^^^^^^^^^^ `std::rc::Rc<()>` cannot be sent between threads safely
+   │ ╭───────────────────'
+26 │ │         let y = x;
+27 │ │         println!("{:?}", y);
+28 │ │     });
+   │ ╰──────' within this `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`
+   │  
+   ┌─ libstd/thread/mod.rs:5:8
+   │
+ 5 │     F: Send + 'static,
+   │        ---- required by this bound in `std::thread::spawn`
+   │
+   = help: within `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<()>`
+   = note: required because it appears within the type `Port<()>`
+   = note: required because it appears within the type `main::Foo`
+   = note: required because it appears within the type `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`
+
+error: aborting due 5 previous errors
+ = Some errors have detailed explanations: E0121, E0277, E0666.
+ = For more information about an error, try `rustc --explain E0121`.
+
+
diff --git a/tests/snapshots/term__overlapping__short_color.snap b/tests/snapshots/term__overlapping__short_color.snap
new file mode 100644 (file)
index 0000000..8e33cbf
--- /dev/null
@@ -0,0 +1,11 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+nested_impl_trait.rs:5:56: {fg:Red bold bright}error[E0666]{bold bright}: nested `impl Trait` is not allowed{/}
+typeck_type_placeholder_item.rs:1:18: {fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
+typeck_type_placeholder_item.rs:2:25: {fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
+typeck_type_placeholder_item.rs:2:28: {fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
+no_send_res_ports.rs:25:5: {fg:Red bold bright}error[E0277]{bold bright}: `std::rc::Rc<()>` cannot be sent between threads safely{/}
+{fg:Red bold bright}error{bold bright}: aborting due 5 previous errors{/}
+
diff --git a/tests/snapshots/term__overlapping__short_no_color.snap b/tests/snapshots/term__overlapping__short_no_color.snap
new file mode 100644 (file)
index 0000000..47ed5db
--- /dev/null
@@ -0,0 +1,11 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+nested_impl_trait.rs:5:56: error[E0666]: nested `impl Trait` is not allowed
+typeck_type_placeholder_item.rs:1:18: error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+typeck_type_placeholder_item.rs:2:25: error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+typeck_type_placeholder_item.rs:2:28: error[E0121]: the type placeholder `_` is not allowed within types on item signatures
+no_send_res_ports.rs:25:5: error[E0277]: `std::rc::Rc<()>` cannot be sent between threads safely
+error: aborting due 5 previous errors
+
diff --git a/tests/snapshots/term__position_indicator__medium_no_color.snap b/tests/snapshots/term__position_indicator__medium_no_color.snap
new file mode 100644 (file)
index 0000000..0e40c83
--- /dev/null
@@ -0,0 +1,7 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+tests/main.js:4:3: warning[ParserWarning]: The strict mode declaration in the body of function `foo` is redundant, as the outer scope is already in strict mode
+
+
diff --git a/tests/snapshots/term__position_indicator__rich_ascii_no_color.snap b/tests/snapshots/term__position_indicator__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..d99ad14
--- /dev/null
@@ -0,0 +1,14 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning[ParserWarning]: The strict mode declaration in the body of function `foo` is redundant, as the outer scope is already in strict mode
+  --> tests/main.js:4:3
+  |
+1 | "use strict";
+  | ------------ Strict mode is first declared here
+  .
+4 |   "use strict";
+  |   ^^^^^^^^^^^^ This strict mode declaration is redundant
+
+
diff --git a/tests/snapshots/term__position_indicator__rich_no_color.snap b/tests/snapshots/term__position_indicator__rich_no_color.snap
new file mode 100644 (file)
index 0000000..35217e4
--- /dev/null
@@ -0,0 +1,14 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning[ParserWarning]: The strict mode declaration in the body of function `foo` is redundant, as the outer scope is already in strict mode
+  ┌─ tests/main.js:4:3
+  │
+1 │ "use strict";
+  │ ------------ Strict mode is first declared here
+  ·
+4 │   "use strict";
+  │   ^^^^^^^^^^^^ This strict mode declaration is redundant
+
+
diff --git a/tests/snapshots/term__position_indicator__short_no_color.snap b/tests/snapshots/term__position_indicator__short_no_color.snap
new file mode 100644 (file)
index 0000000..3ec348d
--- /dev/null
@@ -0,0 +1,6 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+tests/main.js:4:3: warning[ParserWarning]: The strict mode declaration in the body of function `foo` is redundant, as the outer scope is already in strict mode
+
diff --git a/tests/snapshots/term__same_line__medium_color.snap b/tests/snapshots/term__same_line__medium_color.snap
new file mode 100644 (file)
index 0000000..43e1eb0
--- /dev/null
@@ -0,0 +1,8 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+one_line.rs:3:12: {fg:Red bold bright}error[E0499]{bold bright}: cannot borrow `v` as mutable more than once at a time{/}
+{fg:Red bold bright}error{bold bright}: aborting due to previous error{/}
+ {fg:Blue}={/} For more information about this error, try `rustc --explain E0499`.
+
diff --git a/tests/snapshots/term__same_line__medium_no_color.snap b/tests/snapshots/term__same_line__medium_no_color.snap
new file mode 100644 (file)
index 0000000..6bb55ed
--- /dev/null
@@ -0,0 +1,8 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+one_line.rs:3:12: error[E0499]: cannot borrow `v` as mutable more than once at a time
+error: aborting due to previous error
+ = For more information about this error, try `rustc --explain E0499`.
+
diff --git a/tests/snapshots/term__same_line__rich_ascii_no_color.snap b/tests/snapshots/term__same_line__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..60e7afb
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0499]: cannot borrow `v` as mutable more than once at a time
+  --> one_line.rs:3:12
+  |
+3 |     v.push(v.pop().unwrap());
+  |     - ---- ^ second mutable borrow occurs here
+  |     | |     
+  |     | first mutable borrow occurs here
+  |     first borrow later used by call
+
+error: aborting due to previous error
+ = For more information about this error, try `rustc --explain E0499`.
+
+
diff --git a/tests/snapshots/term__same_line__rich_color.snap b/tests/snapshots/term__same_line__rich_color.snap
new file mode 100644 (file)
index 0000000..38addbb
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error[E0499]{bold bright}: cannot borrow `v` as mutable more than once at a time{/}
+  {fg:Blue}┌─{/} one_line.rs:3:12
+  {fg:Blue}│{/}
+{fg:Blue}3{/} {fg:Blue}│{/}     v.push({fg:Red}v{/}.pop().unwrap());
+  {fg:Blue}│{/}     {fg:Blue}-{/} {fg:Blue}----{/} {fg:Red}^{/} {fg:Red}second mutable borrow occurs here{/}
+  {fg:Blue}│{/}     {fg:Blue}│{/} {fg:Blue}│{/}     
+  {fg:Blue}│{/}     {fg:Blue}│{/} {fg:Blue}first mutable borrow occurs here{/}
+  {fg:Blue}│{/}     {fg:Blue}first borrow later used by call{/}
+
+{fg:Red bold bright}error{bold bright}: aborting due to previous error{/}
+ {fg:Blue}={/} For more information about this error, try `rustc --explain E0499`.
+
+
diff --git a/tests/snapshots/term__same_line__rich_no_color.snap b/tests/snapshots/term__same_line__rich_no_color.snap
new file mode 100644 (file)
index 0000000..75511a4
--- /dev/null
@@ -0,0 +1,17 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0499]: cannot borrow `v` as mutable more than once at a time
+  ┌─ one_line.rs:3:12
+  │
+3 │     v.push(v.pop().unwrap());
+  │     - ---- ^ second mutable borrow occurs here
+  │     │ │     
+  │     │ first mutable borrow occurs here
+  │     first borrow later used by call
+
+error: aborting due to previous error
+ = For more information about this error, try `rustc --explain E0499`.
+
+
diff --git a/tests/snapshots/term__same_line__short_color.snap b/tests/snapshots/term__same_line__short_color.snap
new file mode 100644 (file)
index 0000000..c2d86c7
--- /dev/null
@@ -0,0 +1,7 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+one_line.rs:3:12: {fg:Red bold bright}error[E0499]{bold bright}: cannot borrow `v` as mutable more than once at a time{/}
+{fg:Red bold bright}error{bold bright}: aborting due to previous error{/}
+
diff --git a/tests/snapshots/term__same_line__short_no_color.snap b/tests/snapshots/term__same_line__short_no_color.snap
new file mode 100644 (file)
index 0000000..d4f94e1
--- /dev/null
@@ -0,0 +1,7 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+one_line.rs:3:12: error[E0499]: cannot borrow `v` as mutable more than once at a time
+error: aborting due to previous error
+
diff --git a/tests/snapshots/term__same_ranges__medium_color.snap b/tests/snapshots/term__same_ranges__medium_color.snap
new file mode 100644 (file)
index 0000000..095b975
--- /dev/null
@@ -0,0 +1,7 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+same_range:1:5: {fg:Red bold bright}error{bold bright}: Unexpected token{/}
+
+
diff --git a/tests/snapshots/term__same_ranges__medium_no_color.snap b/tests/snapshots/term__same_ranges__medium_no_color.snap
new file mode 100644 (file)
index 0000000..a452022
--- /dev/null
@@ -0,0 +1,7 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+same_range:1:5: error: Unexpected token
+
+
diff --git a/tests/snapshots/term__same_ranges__rich_ascii_no_color.snap b/tests/snapshots/term__same_ranges__rich_ascii_no_color.snap
new file mode 100644 (file)
index 0000000..d64f099
--- /dev/null
@@ -0,0 +1,14 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: Unexpected token
+  --> same_range:1:5
+  |
+1 | ::S { }
+  |     ^
+  |     |
+  |     Unexpected '{'
+  |     Expected '('
+
+
diff --git a/tests/snapshots/term__same_ranges__rich_color.snap b/tests/snapshots/term__same_ranges__rich_color.snap
new file mode 100644 (file)
index 0000000..e6ec885
--- /dev/null
@@ -0,0 +1,14 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+{fg:Red bold bright}error{bold bright}: Unexpected token{/}
+  {fg:Blue}┌─{/} same_range:1:5
+  {fg:Blue}│{/}
+{fg:Blue}1{/} {fg:Blue}│{/} ::S {fg:Red}{{/} }
+  {fg:Blue}│{/}     {fg:Red}^{/}
+  {fg:Blue}│{/}     {fg:Red}│{/}
+  {fg:Blue}│{/}     {fg:Red}Unexpected '{'{/}
+  {fg:Blue}│{/}     {fg:Blue}Expected '('{/}
+
+
diff --git a/tests/snapshots/term__same_ranges__rich_no_color.snap b/tests/snapshots/term__same_ranges__rich_no_color.snap
new file mode 100644 (file)
index 0000000..09510e6
--- /dev/null
@@ -0,0 +1,14 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error: Unexpected token
+  ┌─ same_range:1:5
+  │
+1 │ ::S { }
+  │     ^
+  │     │
+  │     Unexpected '{'
+  │     Expected '('
+
+
diff --git a/tests/snapshots/term__same_ranges__short_color.snap b/tests/snapshots/term__same_ranges__short_color.snap
new file mode 100644 (file)
index 0000000..14ccc40
--- /dev/null
@@ -0,0 +1,6 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_color(&config)
+---
+same_range:1:5: {fg:Red bold bright}error{bold bright}: Unexpected token{/}
+
diff --git a/tests/snapshots/term__same_ranges__short_no_color.snap b/tests/snapshots/term__same_ranges__short_no_color.snap
new file mode 100644 (file)
index 0000000..94bdc59
--- /dev/null
@@ -0,0 +1,6 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+same_range:1:5: error: Unexpected token
+
diff --git a/tests/snapshots/term__tab_columns__tab_width_2_no_color.snap b/tests/snapshots/term__tab_columns__tab_width_2_no_color.snap
new file mode 100644 (file)
index 0000000..0faa8d8
--- /dev/null
@@ -0,0 +1,23 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning: tab test
+  ┌─ tab_columns:1:2
+  │
+1 │   hello
+  │   ^^^^^
+2 │ ∙ hello
+  │   ^^^^^
+3 │ ∙∙  hello
+  │     ^^^^^
+4 │ ∙∙∙ hello
+  │     ^^^^^
+5 │ ∙∙∙∙  hello
+  │       ^^^^^
+6 │ ∙∙∙∙∙ hello
+  │       ^^^^^
+7 │ ∙∙∙∙∙∙  hello
+  │         ^^^^^
+
+
diff --git a/tests/snapshots/term__tab_columns__tab_width_3_no_color.snap b/tests/snapshots/term__tab_columns__tab_width_3_no_color.snap
new file mode 100644 (file)
index 0000000..40f20a6
--- /dev/null
@@ -0,0 +1,23 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning: tab test
+  ┌─ tab_columns:1:2
+  │
+1 │    hello
+  │    ^^^^^
+2 │ ∙  hello
+  │    ^^^^^
+3 │ ∙∙ hello
+  │    ^^^^^
+4 │ ∙∙∙   hello
+  │       ^^^^^
+5 │ ∙∙∙∙  hello
+  │       ^^^^^
+6 │ ∙∙∙∙∙ hello
+  │       ^^^^^
+7 │ ∙∙∙∙∙∙   hello
+  │          ^^^^^
+
+
diff --git a/tests/snapshots/term__tab_columns__tab_width_6_no_color.snap b/tests/snapshots/term__tab_columns__tab_width_6_no_color.snap
new file mode 100644 (file)
index 0000000..018a402
--- /dev/null
@@ -0,0 +1,23 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning: tab test
+  ┌─ tab_columns:1:2
+  │
+1 │       hello
+  │       ^^^^^
+2 │ ∙     hello
+  │       ^^^^^
+3 │ ∙∙    hello
+  │       ^^^^^
+4 │ ∙∙∙   hello
+  │       ^^^^^
+5 │ ∙∙∙∙  hello
+  │       ^^^^^
+6 │ ∙∙∙∙∙ hello
+  │       ^^^^^
+7 │ ∙∙∙∙∙∙      hello
+  │             ^^^^^
+
+
diff --git a/tests/snapshots/term__tab_columns__tab_width_default_no_color.snap b/tests/snapshots/term__tab_columns__tab_width_default_no_color.snap
new file mode 100644 (file)
index 0000000..67cd187
--- /dev/null
@@ -0,0 +1,23 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning: tab test
+  ┌─ tab_columns:1:2
+  │
+1 │     hello
+  │     ^^^^^
+2 │ ∙   hello
+  │     ^^^^^
+3 │ ∙∙  hello
+  │     ^^^^^
+4 │ ∙∙∙ hello
+  │     ^^^^^
+5 │ ∙∙∙∙    hello
+  │         ^^^^^
+6 │ ∙∙∙∙∙   hello
+  │         ^^^^^
+7 │ ∙∙∙∙∙∙  hello
+  │         ^^^^^
+
+
diff --git a/tests/snapshots/term__tabbed__tab_width_3_no_color.snap b/tests/snapshots/term__tabbed__tab_width_3_no_color.snap
new file mode 100644 (file)
index 0000000..f7328c3
--- /dev/null
@@ -0,0 +1,23 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning: unknown weapon `DogJaw`
+  ┌─ tabbed:3:11
+  │
+3 │       Weapon: DogJaw
+  │               ^^^^^^ the weapon
+
+warning: unknown condition `attack-cooldown`
+  ┌─ tabbed:4:23
+  │
+4 │       ReloadingCondition:  attack-cooldown
+  │                            ^^^^^^^^^^^^^^^ the condition
+
+warning: unknown field `Foo`
+  ┌─ tabbed:5:2
+  │
+5 │    Foo: Bar
+  │    ^^^ the field
+
+
diff --git a/tests/snapshots/term__tabbed__tab_width_6_no_color.snap b/tests/snapshots/term__tabbed__tab_width_6_no_color.snap
new file mode 100644 (file)
index 0000000..bcd1b2b
--- /dev/null
@@ -0,0 +1,23 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning: unknown weapon `DogJaw`
+  ┌─ tabbed:3:11
+  │
+3 │             Weapon: DogJaw
+  │                     ^^^^^^ the weapon
+
+warning: unknown condition `attack-cooldown`
+  ┌─ tabbed:4:23
+  │
+4 │             ReloadingCondition:     attack-cooldown
+  │                                     ^^^^^^^^^^^^^^^ the condition
+
+warning: unknown field `Foo`
+  ┌─ tabbed:5:2
+  │
+5 │       Foo: Bar
+  │       ^^^ the field
+
+
diff --git a/tests/snapshots/term__tabbed__tab_width_default_no_color.snap b/tests/snapshots/term__tabbed__tab_width_default_no_color.snap
new file mode 100644 (file)
index 0000000..d3a41a0
--- /dev/null
@@ -0,0 +1,23 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+warning: unknown weapon `DogJaw`
+  ┌─ tabbed:3:11
+  │
+3 │         Weapon: DogJaw
+  │                 ^^^^^^ the weapon
+
+warning: unknown condition `attack-cooldown`
+  ┌─ tabbed:4:23
+  │
+4 │         ReloadingCondition: attack-cooldown
+  │                             ^^^^^^^^^^^^^^^ the condition
+
+warning: unknown field `Foo`
+  ┌─ tabbed:5:2
+  │
+5 │     Foo: Bar
+  │     ^^^ the field
+
+
diff --git a/tests/snapshots/term__unicode__medium_no_color.snap b/tests/snapshots/term__unicode__medium_no_color.snap
new file mode 100644 (file)
index 0000000..b5f8780
--- /dev/null
@@ -0,0 +1,29 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+unicode.rs:1:8: error[E0703]: invalid ABI: found `路濫狼á́́`
+ = valid ABIs:
+     - aapcs
+     - amdgpu-kernel
+     - C
+     - cdecl
+     - efiapi
+     - fastcall
+     - msp430-interrupt
+     - platform-intrinsic
+     - ptx-kernel
+     - Rust
+     - rust-call
+     - rust-intrinsic
+     - stdcall
+     - system
+     - sysv64
+     - thiscall
+     - unadjusted
+     - vectorcall
+     - win64
+     - x86-interrupt
+error: aborting due to previous error
+ = For more information about this error, try `rustc --explain E0703`.
+
diff --git a/tests/snapshots/term__unicode__rich_no_color.snap b/tests/snapshots/term__unicode__rich_no_color.snap
new file mode 100644 (file)
index 0000000..520d10a
--- /dev/null
@@ -0,0 +1,36 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E0703]: invalid ABI: found `路濫狼á́́`
+  ┌─ unicode.rs:1:8
+  │
+1 │ extern "路濫狼á́́" fn foo() {}
+  │        ^^^^^^^^^ invalid ABI
+  │
+  = valid ABIs:
+      - aapcs
+      - amdgpu-kernel
+      - C
+      - cdecl
+      - efiapi
+      - fastcall
+      - msp430-interrupt
+      - platform-intrinsic
+      - ptx-kernel
+      - Rust
+      - rust-call
+      - rust-intrinsic
+      - stdcall
+      - system
+      - sysv64
+      - thiscall
+      - unadjusted
+      - vectorcall
+      - win64
+      - x86-interrupt
+
+error: aborting due to previous error
+ = For more information about this error, try `rustc --explain E0703`.
+
+
diff --git a/tests/snapshots/term__unicode__short_no_color.snap b/tests/snapshots/term__unicode__short_no_color.snap
new file mode 100644 (file)
index 0000000..ad1dae1
--- /dev/null
@@ -0,0 +1,7 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+unicode.rs:1:8: error[E0703]: invalid ABI: found `路濫狼á́́`
+error: aborting due to previous error
+
diff --git a/tests/snapshots/term__unicode_spans__medium_no_color.snap b/tests/snapshots/term__unicode_spans__medium_no_color.snap
new file mode 100644 (file)
index 0000000..1b19451
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+moon_jump.rs:1:1: error[E01]: cow may not jump during new moon.
+note: invalid unicode range
+note: invalid unicode range
+note: invalid unicode range
+
diff --git a/tests/snapshots/term__unicode_spans__rich_no_color.snap b/tests/snapshots/term__unicode_spans__rich_no_color.snap
new file mode 100644 (file)
index 0000000..653a60e
--- /dev/null
@@ -0,0 +1,29 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[E01]: cow may not jump during new moon.
+  ┌─ moon_jump.rs:1:1
+  │
+1 │ 🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄
+  │ ^^ Invalid jump
+
+note: invalid unicode range
+  ┌─ moon_jump.rs:1:1
+  │
+1 │ 🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄
+  │ -- Cow range does not start at boundary.
+
+note: invalid unicode range
+  ┌─ moon_jump.rs:1:3
+  │
+1 │ 🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄
+  │     -- Cow range does not end at boundary.
+
+note: invalid unicode range
+  ┌─ moon_jump.rs:1:1
+  │
+1 │ 🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄
+  │ ------ Cow does not start or end at boundary.
+
+
diff --git a/tests/snapshots/term__unicode_spans__short_no_color.snap b/tests/snapshots/term__unicode_spans__short_no_color.snap
new file mode 100644 (file)
index 0000000..1b19451
--- /dev/null
@@ -0,0 +1,9 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+moon_jump.rs:1:1: error[E01]: cow may not jump during new moon.
+note: invalid unicode range
+note: invalid unicode range
+note: invalid unicode range
+
diff --git a/tests/support/color_buffer.rs b/tests/support/color_buffer.rs
new file mode 100644 (file)
index 0000000..b51360c
--- /dev/null
@@ -0,0 +1,137 @@
+use std::io;
+use std::io::prelude::*;
+use termcolor::{ColorSpec, WriteColor};
+
+// Color tester from:
+// https://github.com/wycats/language-reporting/blob/b021c87e0d4916b5f32756151bf215c220eee52d/crates/render-tree/src/stylesheet/accumulator.rs
+
+/// A facility for creating visually inspectable representations of colored output
+/// so they can be easily tested.
+///
+/// A new color is represented as `{style}` and a reset is represented by `{/}`.
+///
+/// Attributes are printed in this order:
+///
+/// - Foreground color as `fg:Color`
+/// - Background color as `bg:Color`
+/// - Bold as `bold`
+/// - Underline as `underline`
+/// - Intense as `bright`
+///
+/// For example, the style "intense, bold red foreground" would be printed as:
+///
+/// ```text
+/// {fg:Red bold intense}
+/// ```
+///
+/// Since this implementation attempts to make it possible to faithfully
+/// understand what real WriteColor implementations would do, it tries
+/// to approximate the contract in the WriteColor trait: "Subsequent
+/// writes to this write will use these settings until either reset is
+/// called or new color settings are set.")
+///
+/// - If set_color is called with a style, `{...}` is emitted containing the
+///   color attributes.
+/// - If set_color is called with no style, `{/}` is emitted
+/// - If reset is called, `{/}` is emitted.
+pub struct ColorBuffer {
+    buf: Vec<u8>,
+    color: ColorSpec,
+}
+
+impl ColorBuffer {
+    pub fn new() -> ColorBuffer {
+        ColorBuffer {
+            buf: Vec::new(),
+            color: ColorSpec::new(),
+        }
+    }
+
+    pub fn into_string(self) -> String {
+        String::from_utf8(self.buf).unwrap()
+    }
+}
+
+impl io::Write for ColorBuffer {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.buf.extend(buf);
+        Ok(buf.len())
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}
+
+impl WriteColor for ColorBuffer {
+    fn supports_color(&self) -> bool {
+        true
+    }
+
+    fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
+        #![allow(unused_assignments)]
+
+        if self.color == *spec {
+            return Ok(());
+        } else {
+            self.color = spec.clone();
+        }
+
+        if spec.is_none() {
+            write!(self, "{{/}}")?;
+            return Ok(());
+        } else {
+            write!(self, "{{")?;
+        }
+
+        let mut first = true;
+
+        fn write_first(first: bool, write: &mut ColorBuffer) -> io::Result<bool> {
+            if !first {
+                write!(write, " ")?;
+            }
+
+            Ok(false)
+        };
+
+        if let Some(fg) = spec.fg() {
+            first = write_first(first, self)?;
+            write!(self, "fg:{:?}", fg)?;
+        }
+
+        if let Some(bg) = spec.bg() {
+            first = write_first(first, self)?;
+            write!(self, "bg:{:?}", bg)?;
+        }
+
+        if spec.bold() {
+            first = write_first(first, self)?;
+            write!(self, "bold")?;
+        }
+
+        if spec.underline() {
+            first = write_first(first, self)?;
+            write!(self, "underline")?;
+        }
+
+        if spec.intense() {
+            first = write_first(first, self)?;
+            write!(self, "bright")?;
+        }
+
+        write!(self, "}}")?;
+
+        Ok(())
+    }
+
+    fn reset(&mut self) -> io::Result<()> {
+        let color = self.color.clone();
+
+        if color != ColorSpec::new() {
+            write!(self, "{{/}}")?;
+            self.color = ColorSpec::new();
+        }
+
+        Ok(())
+    }
+}
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
new file mode 100644 (file)
index 0000000..4f7313a
--- /dev/null
@@ -0,0 +1,31 @@
+use codespan_reporting::diagnostic::Diagnostic;
+use codespan_reporting::files::Files;
+use codespan_reporting::term::{emit, Config};
+use termcolor::{Buffer, WriteColor};
+
+mod color_buffer;
+
+use self::color_buffer::ColorBuffer;
+
+pub struct TestData<'files, F: Files<'files>> {
+    pub files: F,
+    pub diagnostics: Vec<Diagnostic<F::FileId>>,
+}
+
+impl<'files, F: Files<'files>> TestData<'files, F> {
+    fn emit<W: WriteColor>(&'files self, mut writer: W, config: &Config) -> W {
+        for diagnostic in &self.diagnostics {
+            emit(&mut writer, config, &self.files, &diagnostic).unwrap();
+        }
+        writer
+    }
+
+    pub fn emit_color(&'files self, config: &Config) -> String {
+        self.emit(ColorBuffer::new(), &config).into_string()
+    }
+
+    pub fn emit_no_color(&'files self, config: &Config) -> String {
+        let buffer = self.emit(Buffer::no_color(), &config);
+        String::from_utf8_lossy(buffer.as_slice()).into_owned()
+    }
+}
diff --git a/tests/term.rs b/tests/term.rs
new file mode 100644 (file)
index 0000000..035db9b
--- /dev/null
@@ -0,0 +1,1053 @@
+use codespan_reporting::diagnostic::{Diagnostic, Label};
+use codespan_reporting::files::{SimpleFile, SimpleFiles};
+use codespan_reporting::term::{termcolor::Color, Chars, Config, DisplayStyle, Styles};
+
+mod support;
+
+use self::support::TestData;
+
+lazy_static::lazy_static! {
+    static ref TEST_CONFIG: Config = Config {
+        // Always use blue so tests are consistent across platforms
+        styles: Styles::with_blue(Color::Blue),
+        ..Config::default()
+    };
+}
+
+macro_rules! test_emit {
+    (rich_color) => {
+        #[test]
+        fn rich_color() {
+            let config = Config {
+                display_style: DisplayStyle::Rich,
+                ..TEST_CONFIG.clone()
+            };
+
+            insta::assert_snapshot!(TEST_DATA.emit_color(&config));
+        }
+    };
+    (medium_color) => {
+        #[test]
+        fn medium_color() {
+            let config = Config {
+                display_style: DisplayStyle::Medium,
+                ..TEST_CONFIG.clone()
+            };
+
+            insta::assert_snapshot!(TEST_DATA.emit_color(&config));
+        }
+    };
+    (short_color) => {
+        #[test]
+        fn short_color() {
+            let config = Config {
+                display_style: DisplayStyle::Short,
+                ..TEST_CONFIG.clone()
+            };
+
+            insta::assert_snapshot!(TEST_DATA.emit_color(&config));
+        }
+    };
+    (rich_no_color) => {
+        #[test]
+        fn rich_no_color() {
+            let config = Config {
+                display_style: DisplayStyle::Rich,
+                ..TEST_CONFIG.clone()
+            };
+
+            insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+        }
+    };
+    (medium_no_color) => {
+        #[test]
+        fn medium_no_color() {
+            let config = Config {
+                display_style: DisplayStyle::Medium,
+                ..TEST_CONFIG.clone()
+            };
+
+            insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+        }
+    };
+    (short_no_color) => {
+        #[test]
+        fn short_no_color() {
+            let config = Config {
+                display_style: DisplayStyle::Short,
+                ..TEST_CONFIG.clone()
+            };
+
+            insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+        }
+    };
+    (rich_ascii_no_color) => {
+        #[test]
+        fn rich_ascii_no_color() {
+            let config = Config {
+                display_style: DisplayStyle::Rich,
+                chars: Chars::ascii(),
+                ..TEST_CONFIG.clone()
+            };
+
+            insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+        }
+    };
+}
+
+mod empty {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, &'static str>> = {
+            let files = SimpleFiles::new();
+
+            let diagnostics = vec![
+                Diagnostic::bug(),
+                Diagnostic::error(),
+                Diagnostic::warning(),
+                Diagnostic::note(),
+                Diagnostic::help(),
+                Diagnostic::bug(),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+/// Based on:
+/// - https://github.com/rust-lang/rust/blob/c20d7eecbc0928b57da8fe30b2ef8528e2bdd5be/src/test/ui/codemap_tests/one_line.stderr
+mod same_line {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
+            let mut files = SimpleFiles::new();
+
+            let file_id1 = files.add(
+                "one_line.rs",
+                unindent::unindent(r#"
+                    fn main() {
+                        let mut v = vec![Some("foo"), Some("bar")];
+                        v.push(v.pop().unwrap());
+                    }
+                "#),
+            );
+
+            let diagnostics = vec![
+                Diagnostic::error()
+                    .with_code("E0499")
+                    .with_message("cannot borrow `v` as mutable more than once at a time")
+                    .with_labels(vec![
+                        Label::primary(file_id1, 71..72)
+                            .with_message("second mutable borrow occurs here"),
+                        Label::secondary(file_id1, 64..65)
+                            .with_message("first borrow later used by call"),
+                        Label::secondary(file_id1, 66..70)
+                            .with_message("first mutable borrow occurs here"),
+                    ]),
+                Diagnostic::error()
+                    .with_message("aborting due to previous error")
+                    .with_notes(vec![
+                        "For more information about this error, try `rustc --explain E0499`.".to_owned(),
+                    ]),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+/// Based on:
+/// - https://github.com/rust-lang/rust/blob/c20d7eecbc0928b57da8fe30b2ef8528e2bdd5be/src/test/ui/nested_impl_trait.stderr
+/// - https://github.com/rust-lang/rust/blob/c20d7eecbc0928b57da8fe30b2ef8528e2bdd5be/src/test/ui/typeck/typeck_type_placeholder_item.stderr
+/// - https://github.com/rust-lang/rust/blob/c20d7eecbc0928b57da8fe30b2ef8528e2bdd5be/src/test/ui/no_send_res_ports.stderr
+mod overlapping {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
+            let mut files = SimpleFiles::new();
+
+            let file_id1 = files.add(
+                "nested_impl_trait.rs",
+                unindent::unindent(r#"
+                    use std::fmt::Debug;
+
+                    fn fine(x: impl Into<u32>) -> impl Into<u32> { x }
+
+                    fn bad_in_ret_position(x: impl Into<u32>) -> impl Into<impl Debug> { x }
+                "#),
+            );
+            let file_id2 = files.add(
+                "typeck_type_placeholder_item.rs",
+                unindent::unindent(r#"
+                    fn fn_test1() -> _ { 5 }
+                    fn fn_test2(x: i32) -> (_, _) { (x, x) }
+                "#),
+            );
+            let file_id3 = files.add(
+                "libstd/thread/mod.rs",
+                unindent::unindent(r#"
+                    #[stable(feature = "rust1", since = "1.0.0")]
+                    pub fn spawn<F, T>(self, f: F) -> io::Result<JoinHandle<T>>
+                    where
+                        F: FnOnce() -> T,
+                        F: Send + 'static,
+                        T: Send + 'static,
+                    {
+                        unsafe { self.spawn_unchecked(f) }
+                    }
+                "#),
+            );
+            let file_id4 = files.add(
+                "no_send_res_ports.rs",
+                unindent::unindent(r#"
+                    use std::thread;
+                    use std::rc::Rc;
+
+                    #[derive(Debug)]
+                    struct Port<T>(Rc<T>);
+
+                    fn main() {
+                        #[derive(Debug)]
+                        struct Foo {
+                            _x: Port<()>,
+                        }
+
+                        impl Drop for Foo {
+                            fn drop(&mut self) {}
+                        }
+
+                        fn foo(x: Port<()>) -> Foo {
+                            Foo {
+                                _x: x
+                            }
+                        }
+
+                        let x = foo(Port(Rc::new(())));
+
+                        thread::spawn(move|| {
+                            let y = x;
+                            println!("{:?}", y);
+                        });
+                    }
+                "#),
+            );
+
+            let diagnostics = vec![
+                Diagnostic::error()
+                    .with_code("E0666")
+                    .with_message("nested `impl Trait` is not allowed")
+                    .with_labels(vec![
+                        Label::primary(file_id1, 129..139)
+                            .with_message("nested `impl Trait` here"),
+                        Label::secondary(file_id1, 119..140)
+                            .with_message("outer `impl Trait`"),
+                    ]),
+                Diagnostic::error()
+                    .with_code("E0121")
+                    .with_message("the type placeholder `_` is not allowed within types on item signatures")
+                        .with_labels(vec![
+                            Label::primary(file_id2, 17..18)
+                                .with_message("not allowed in type signatures"),
+                            Label::secondary(file_id2, 17..18)
+                                .with_message("help: replace with the correct return type: `i32`"),
+                        ]),
+                Diagnostic::error()
+                    .with_code("E0121")
+                    .with_message("the type placeholder `_` is not allowed within types on item signatures")
+                        .with_labels(vec![
+                            Label::primary(file_id2, 49..50)
+                                .with_message("not allowed in type signatures"),
+                            Label::primary(file_id2, 52..53)
+                                .with_message("not allowed in type signatures"),
+                            Label::secondary(file_id2, 48..54)
+                                .with_message("help: replace with the correct return type: `(i32, i32)`"),
+                        ]),
+                Diagnostic::error()
+                    .with_code("E0277")
+                    .with_message("`std::rc::Rc<()>` cannot be sent between threads safely")
+                    .with_labels(vec![
+                        Label::primary(file_id4, 339..352)
+                            .with_message("`std::rc::Rc<()>` cannot be sent between threads safely"),
+                        Label::secondary(file_id4, 353..416)
+                            .with_message("within this `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`"),
+                        Label::secondary(file_id3, 141..145)
+                            .with_message("required by this bound in `std::thread::spawn`"),
+                    ])
+                    .with_notes(vec![
+                        "help: within `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<()>`".to_owned(),
+                        "note: required because it appears within the type `Port<()>`".to_owned(),
+                        "note: required because it appears within the type `main::Foo`".to_owned(),
+                        "note: required because it appears within the type `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`".to_owned(),
+                    ]),
+                Diagnostic::error()
+                    .with_message("aborting due 5 previous errors")
+                    .with_notes(vec![
+                        "Some errors have detailed explanations: E0121, E0277, E0666.".to_owned(),
+                        "For more information about an error, try `rustc --explain E0121`.".to_owned(),
+                    ]),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod message {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, &'static str>> = {
+            let files = SimpleFiles::new();
+
+            let diagnostics = vec![
+                Diagnostic::error().with_message("a message"),
+                Diagnostic::warning().with_message("a message"),
+                Diagnostic::note().with_message("a message"),
+                Diagnostic::help().with_message("a message"),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod message_and_notes {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, &'static str>> = {
+            let files = SimpleFiles::new();
+
+            let diagnostics = vec![
+                Diagnostic::error().with_message("a message").with_notes(vec!["a note".to_owned()]),
+                Diagnostic::warning().with_message("a message").with_notes(vec!["a note".to_owned()]),
+                Diagnostic::note().with_message("a message").with_notes(vec!["a note".to_owned()]),
+                Diagnostic::help().with_message("a message").with_notes(vec!["a note".to_owned()]),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod message_errorcode {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, &'static str>> = {
+            let files = SimpleFiles::new();
+
+            let diagnostics = vec![
+                Diagnostic::error().with_message("a message").with_code("E0001"),
+                Diagnostic::warning().with_message("a message").with_code("W001"),
+                Diagnostic::note().with_message("a message").with_code("N0815"),
+                Diagnostic::help().with_message("a message").with_code("H4711"),
+                Diagnostic::error().with_message("where did my errorcode go?").with_code(""),
+                Diagnostic::warning().with_message("where did my errorcode go?").with_code(""),
+                Diagnostic::note().with_message("where did my errorcode go?").with_code(""),
+                Diagnostic::help().with_message("where did my errorcode go?").with_code(""),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod empty_ranges {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFile<&'static str, &'static str>> = {
+            let file = SimpleFile::new("hello", "Hello world!\nBye world!\n   ");
+            let eof = file.source().len();
+
+            let diagnostics = vec![
+                Diagnostic::note()
+                    .with_message("middle")
+                    .with_labels(vec![Label::primary((), 6..6).with_message("middle")]),
+                Diagnostic::note()
+                    .with_message("end of line")
+                    .with_labels(vec![Label::primary((), 12..12).with_message("end of line")]),
+                Diagnostic::note()
+                    .with_message("end of line")
+                    .with_labels(vec![Label::primary((), 23..23).with_message("end of line")]),
+                Diagnostic::note()
+                    .with_message("end of file")
+                    .with_labels(vec![Label::primary((), eof..eof).with_message("end of file")]),
+            ];
+
+            TestData { files: file, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod same_ranges {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFile<&'static str, &'static str>> = {
+            let file = SimpleFile::new("same_range", "::S { }");
+
+            let diagnostics = vec![
+                Diagnostic::error()
+                    .with_message("Unexpected token")
+                    .with_labels(vec![
+                        Label::primary((), 4..4).with_message("Unexpected '{'"),
+                        Label::secondary((), 4..4).with_message("Expected '('"),
+                    ]),
+            ];
+
+            TestData { files: file, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod multifile {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
+            let mut files = SimpleFiles::new();
+
+            let file_id1 = files.add(
+                "Data/Nat.fun",
+                unindent::unindent(
+                    "
+                        module Data.Nat where
+
+                        data Nat : Type where
+                            zero : Nat
+                            succ : Nat → Nat
+
+                        {-# BUILTIN NATRAL Nat #-}
+
+                        infixl 6 _+_ _-_
+
+                        _+_ : Nat → Nat → Nat
+                        zero    + n₂ = n₂
+                        succ n₁ + n₂ = succ (n₁ + n₂)
+
+                        _-_ : Nat → Nat → Nat
+                        n₁      - zero    = n₁
+                        zero    - succ n₂ = zero
+                        succ n₁ - succ n₂ = n₁ - n₂
+                    ",
+                ),
+            );
+
+            let file_id2 = files.add(
+                "Test.fun",
+                unindent::unindent(
+                    r#"
+                        module Test where
+
+                        _ : Nat
+                        _ = 123 + "hello"
+                    "#,
+                ),
+            );
+
+            let diagnostics = vec![
+                // Unknown builtin error
+                Diagnostic::error()
+                    .with_message("unknown builtin: `NATRAL`")
+                    .with_labels(vec![Label::primary(file_id1, 96..102).with_message("unknown builtin")])
+                    .with_notes(vec![
+                        "there is a builtin with a similar name: `NATURAL`".to_owned(),
+                    ]),
+                // Unused parameter warning
+                Diagnostic::warning()
+                    .with_message("unused parameter pattern: `n₂`")
+                    .with_labels(vec![Label::primary(file_id1, 285..289).with_message("unused parameter")])
+                    .with_notes(vec!["consider using a wildcard pattern: `_`".to_owned()]),
+                // Unexpected type error
+                Diagnostic::error()
+                    .with_message("unexpected type in application of `_+_`")
+                    .with_code("E0001")
+                    .with_labels(vec![
+                        Label::primary(file_id2, 37..44).with_message("expected `Nat`, found `String`"),
+                        Label::secondary(file_id1, 130..155).with_message("based on the definition of `_+_`"),
+                    ])
+                    .with_notes(vec![unindent::unindent(
+                        "
+                            expected type `Nat`
+                               found type `String`
+                        ",
+                    )]),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod fizz_buzz {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
+            let mut files = SimpleFiles::new();
+
+            let file_id = files.add(
+                "FizzBuzz.fun",
+                unindent::unindent(
+                    r#"
+                        module FizzBuzz where
+
+                        fizz₁ : Nat → String
+                        fizz₁ num = case (mod num 5) (mod num 3) of
+                            0 0 => "FizzBuzz"
+                            0 _ => "Fizz"
+                            _ 0 => "Buzz"
+                            _ _ => num
+
+                        fizz₂ : Nat → String
+                        fizz₂ num =
+                            case (mod num 5) (mod num 3) of
+                                0 0 => "FizzBuzz"
+                                0 _ => "Fizz"
+                                _ 0 => "Buzz"
+                                _ _ => num
+                    "#,
+                ),
+            );
+
+            let diagnostics = vec![
+                // Incompatible match clause error
+                Diagnostic::error()
+                    .with_message("`case` clauses have incompatible types")
+                    .with_code("E0308")
+                    .with_labels(vec![
+                        Label::primary(file_id, 163..166).with_message("expected `String`, found `Nat`"),
+                        Label::secondary(file_id, 62..166).with_message("`case` clauses have incompatible types"),
+                        Label::secondary(file_id, 41..47).with_message("expected type `String` found here"),
+                    ])
+                    .with_notes(vec![unindent::unindent(
+                        "
+                            expected type `String`
+                               found type `Nat`
+                        ",
+                    )]),
+                // Incompatible match clause error
+                Diagnostic::error()
+                    .with_message("`case` clauses have incompatible types")
+                    .with_code("E0308")
+                    .with_labels(vec![
+                        Label::primary(file_id, 328..331).with_message("expected `String`, found `Nat`"),
+                        Label::secondary(file_id, 211..331).with_message("`case` clauses have incompatible types"),
+                        Label::secondary(file_id, 258..268).with_message("this is found to be of type `String`"),
+                        Label::secondary(file_id, 284..290).with_message("this is found to be of type `String`"),
+                        Label::secondary(file_id, 306..312).with_message("this is found to be of type `String`"),
+                        Label::secondary(file_id, 186..192).with_message("expected type `String` found here"),
+                    ])
+                    .with_notes(vec![unindent::unindent(
+                        "
+                            expected type `String`
+                               found type `Nat`
+                        ",
+                    )]),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod multiline_overlapping {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFile<&'static str, String>> = {
+            let file = SimpleFile::new(
+                "codespan/src/file.rs",
+                [
+                    "        match line_index.compare(self.last_line_index()) {",
+                    "            Ordering::Less => Ok(self.line_starts()[line_index.to_usize()]),",
+                    "            Ordering::Equal => Ok(self.source_span().end()),",
+                    "            Ordering::Greater => LineIndexOutOfBoundsError {",
+                    "                given: line_index,",
+                    "                max: self.last_line_index(),",
+                    "            },",
+                    "        }",
+                ].join("\n"),
+            );
+
+            let diagnostics = vec![
+                Diagnostic::error()
+                    .with_message("match arms have incompatible types")
+                    .with_code("E0308")
+                    .with_labels(vec![
+                        // this secondary label is before the primary label to test the locus calculation (see issue #259)
+                        Label::secondary((), 89..134).with_message("this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`"),
+                        Label::primary((), 230..351).with_message("expected enum `Result`, found struct `LineIndexOutOfBoundsError`"),
+                        Label::secondary((), 8..362).with_message("`match` arms have incompatible types"),
+                        Label::secondary((), 167..195).with_message("this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`"),
+                    ])
+                    .with_notes(vec![unindent::unindent(
+                        "
+                            expected type `Result<ByteIndex, LineIndexOutOfBoundsError>`
+                               found type `LineIndexOutOfBoundsError`
+                        ",
+                    )]),
+            ];
+
+            TestData { files: file, diagnostics }
+        };
+    }
+
+    test_emit!(rich_color);
+    test_emit!(medium_color);
+    test_emit!(short_color);
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod tabbed {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
+            let mut files = SimpleFiles::new();
+
+            let file_id = files.add(
+                "tabbed",
+                [
+                    "Entity:",
+                    "\tArmament:",
+                    "\t\tWeapon: DogJaw",
+                    "\t\tReloadingCondition:\tattack-cooldown",
+                    "\tFoo: Bar",
+                ]
+                .join("\n"),
+            );
+
+            let diagnostics = vec![
+                Diagnostic::warning()
+                    .with_message("unknown weapon `DogJaw`")
+                    .with_labels(vec![Label::primary(file_id, 29..35).with_message("the weapon")]),
+                Diagnostic::warning()
+                    .with_message("unknown condition `attack-cooldown`")
+                    .with_labels(vec![Label::primary(file_id, 58..73).with_message("the condition")]),
+                Diagnostic::warning()
+                    .with_message("unknown field `Foo`")
+                    .with_labels(vec![Label::primary(file_id, 75..78).with_message("the field")]),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    #[test]
+    fn tab_width_default_no_color() {
+        let config = TEST_CONFIG.clone();
+
+        insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+    }
+
+    #[test]
+    fn tab_width_3_no_color() {
+        let config = Config {
+            tab_width: 3,
+            ..TEST_CONFIG.clone()
+        };
+
+        insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+    }
+
+    #[test]
+    fn tab_width_6_no_color() {
+        let config = Config {
+            tab_width: 6,
+            ..TEST_CONFIG.clone()
+        };
+
+        insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+    }
+}
+
+mod tab_columns {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
+            let mut files = SimpleFiles::new();
+
+            let source = [
+                "\thello",
+                "∙\thello",
+                "∙∙\thello",
+                "∙∙∙\thello",
+                "∙∙∙∙\thello",
+                "∙∙∙∙∙\thello",
+                "∙∙∙∙∙∙\thello",
+            ].join("\n");
+            let hello_ranges = source
+                .match_indices("hello")
+                .map(|(start, hello)| start..(start+hello.len()))
+                .collect::<Vec<_>>();
+
+            let file_id = files.add("tab_columns", source);
+
+            let diagnostics = vec![
+                Diagnostic::warning()
+                    .with_message("tab test")
+                    .with_labels(
+                        hello_ranges
+                            .into_iter()
+                            .map(|range| Label::primary(file_id, range))
+                            .collect(),
+                    ),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    #[test]
+    fn tab_width_default_no_color() {
+        let config = TEST_CONFIG.clone();
+
+        insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+    }
+
+    #[test]
+    fn tab_width_2_no_color() {
+        let config = Config {
+            tab_width: 2,
+            ..TEST_CONFIG.clone()
+        };
+
+        insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+    }
+
+    #[test]
+    fn tab_width_3_no_color() {
+        let config = Config {
+            tab_width: 3,
+            ..TEST_CONFIG.clone()
+        };
+
+        insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+    }
+
+    #[test]
+    fn tab_width_6_no_color() {
+        let config = Config {
+            tab_width: 6,
+            ..TEST_CONFIG.clone()
+        };
+
+        insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
+    }
+}
+
+/// Based on:
+/// - https://github.com/TheSamsa/rust/blob/75cf41afb468152611212271bae026948cd3ba46/src/test/ui/codemap_tests/unicode.stderr
+mod unicode {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFile<&'static str, String>> = {
+            let prefix = r#"extern "#;
+            let abi = r#""路濫狼á́́""#;
+            let suffix = r#" fn foo() {}"#;
+
+            let file = SimpleFile::new(
+                "unicode.rs",
+                format!("{}{}{}", prefix, abi, suffix),
+            );
+
+            let diagnostics = vec![
+                Diagnostic::error()
+                    .with_code("E0703")
+                    .with_message("invalid ABI: found `路濫狼á́́`")
+                    .with_labels(vec![
+                        Label::primary((), prefix.len()..(prefix.len() + abi.len()))
+                            .with_message("invalid ABI"),
+                    ])
+                    .with_notes(vec![unindent::unindent(
+                        "
+                            valid ABIs:
+                              - aapcs
+                              - amdgpu-kernel
+                              - C
+                              - cdecl
+                              - efiapi
+                              - fastcall
+                              - msp430-interrupt
+                              - platform-intrinsic
+                              - ptx-kernel
+                              - Rust
+                              - rust-call
+                              - rust-intrinsic
+                              - stdcall
+                              - system
+                              - sysv64
+                              - thiscall
+                              - unadjusted
+                              - vectorcall
+                              - win64
+                              - x86-interrupt
+                        ",
+                    )]),
+                Diagnostic::error()
+                    .with_message("aborting due to previous error")
+                    .with_notes(vec![
+                        "For more information about this error, try `rustc --explain E0703`.".to_owned(),
+                    ]),
+            ];
+
+            TestData { files: file, diagnostics }
+        };
+    }
+
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+}
+
+mod unicode_spans {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFile<&'static str, String>> = {
+            let moon_phases = format!("{}", r#"🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄"#);
+            let invalid_start = 1;
+            let invalid_end = "🐄".len() - 1;
+            assert_eq!(moon_phases.is_char_boundary(invalid_start), false);
+            assert_eq!(moon_phases.is_char_boundary(invalid_end), false);
+            assert_eq!("🐄".len(), 4);
+            let file = SimpleFile::new(
+                "moon_jump.rs",
+                moon_phases,
+            );
+            let diagnostics = vec![
+                Diagnostic::error()
+                    .with_code("E01")
+                    .with_message("cow may not jump during new moon.")
+                    .with_labels(vec![
+                        Label::primary((), invalid_start..invalid_end)
+                            .with_message("Invalid jump"),
+                    ]),
+                Diagnostic::note()
+                    .with_message("invalid unicode range")
+                    .with_labels(vec![
+                        Label::secondary((), invalid_start.."🐄".len())
+                            .with_message("Cow range does not start at boundary."),
+                    ]),
+                Diagnostic::note()
+                    .with_message("invalid unicode range")
+                    .with_labels(vec![
+                        Label::secondary((), "🐄🌑".len().."🐄🌑🐄".len() - 1)
+                            .with_message("Cow range does not end at boundary."),
+                    ]),
+                Diagnostic::note()
+                    .with_message("invalid unicode range")
+                    .with_labels(vec![
+                        Label::secondary((), invalid_start.."🐄🌑🐄".len() - 1)
+                            .with_message("Cow does not start or end at boundary."),
+                    ]),
+            ];
+            TestData{files: file, diagnostics }
+        };
+    }
+
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+}
+
+mod position_indicator {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_DATA: TestData<'static, SimpleFile<&'static str, String>> = {
+            let file = SimpleFile::new(
+                "tests/main.js",
+                [
+                    "\"use strict\";",
+                    "let zero=0;",
+                    "function foo() {",
+                    "  \"use strict\";",
+                    "  one=1;",
+                    "}",
+                ].join("\n"),
+            );
+            let diagnostics = vec![
+                Diagnostic::warning()
+                    .with_code("ParserWarning")
+                    .with_message("The strict mode declaration in the body of function `foo` is redundant, as the outer scope is already in strict mode")
+                    .with_labels(vec![
+                        Label::primary((), 45..57)
+                            .with_message("This strict mode declaration is redundant"),
+                        Label::secondary((), 0..12)
+                            .with_message("Strict mode is first declared here"),
+                    ]),
+            ];
+            TestData{files: file, diagnostics }
+        };
+    }
+
+    test_emit!(rich_no_color);
+    test_emit!(medium_no_color);
+    test_emit!(short_no_color);
+    test_emit!(rich_ascii_no_color);
+}
+
+mod multiline_omit {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_CONFIG: Config = Config {
+            styles: Styles::with_blue(Color::Blue),
+            start_context_lines: 2,
+            end_context_lines: 1,
+            ..Config::default()
+        };
+
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
+            let mut files = SimpleFiles::new();
+
+            let file_id1 = files.add(
+                "empty_if_comments.lua",
+                [
+                    "elseif 3 then", // primary label starts here
+                    "",              // context line
+                    "",
+                    "",
+                    "",
+                    "",
+                    "",
+                    "",
+                    "",     // context line
+                    "else", // primary label ends here
+                ]
+                .join("\n"),
+            );
+
+            let file_id2 = files.add(
+                "src/lib.rs",
+                [
+                    "fn main() {",
+                    "    1",   // primary label starts here
+                    "    + 1", // context line
+                    "    + 1", // skip
+                    "    + 1", // skip
+                    "    + 1", // skip
+                    "    +1",  // secondary label here
+                    "    + 1", // this single line will not be skipped; the previously filtered out label must be retrieved
+                    "    + 1", // context line
+                    "    + 1", // primary label ends here
+                    "}",
+                ]
+                .join("\n"),
+            );
+
+            let diagnostics = vec![
+                Diagnostic::error()
+                    .with_message("empty elseif block")
+                    .with_code("empty_if")
+                    .with_labels(vec![
+                        Label::primary(file_id1, 0..23),
+                        Label::secondary(file_id1, 15..21).with_message("content should be in here"),
+                    ]),
+                Diagnostic::error()
+                    .with_message("mismatched types")
+                    .with_code("E0308")
+                    .with_labels(vec![
+                        Label::primary(file_id2, 17..80).with_message("expected (), found integer"),
+                        Label::secondary(file_id2, 55..55).with_message("missing whitespace"),
+                    ])
+                    .with_notes(vec![
+                        "note:\texpected type `()`\n\tfound type `{integer}`".to_owned()
+                    ]),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_no_color);
+}