Update miette to 6.0.1

Test: m rust
Change-Id: Id2d0f05405e31650a643ab92f7bcf6880bd9b49c
diff --git a/crates/miette-derive/.android-checksum.json b/crates/miette-derive/.android-checksum.json
index 4be151a..d0f5ea0 100644
--- a/crates/miette-derive/.android-checksum.json
+++ b/crates/miette-derive/.android-checksum.json
@@ -1 +1 @@
-{"package":null,"files":{".cargo-checksum.json":"56c350001d94f4355e68ade15e96076a589a5418655da5a41f8a26a0d5d4e1ca","Android.bp":"ba150e2abdf659fddec9fb612c5d81547042ba2b7647aa2f9981d0aba204ef3c","Cargo.toml":"7063a079d674da607fd4f2edd787423ca20a124b6648e99f4867ced77eb40fd1","LICENSE":"d4e2ffa43f59103b9893849a22699ec727d5898d7081098310750135c4bf0134","METADATA":"6c2260f22248d406f5fe802f8cb435509469df86074eb65679a9284b862e8b0a","MODULE_LICENSE_APACHE2":"0d6f8afa3940b7f06bebee651376d43bc8b0d5b437337be2696d30377451e93a","cargo_embargo.json":"d17b8bd25b48dae259fb01e76a2efeb790fc436fe351c2ac40912800d1647777","src/code.rs":"ff1c86d9cbe87f54f0de905a5274b4c703a228168b64753a6bf3acfe1bd18356","src/diagnostic.rs":"04811edb4086d614f727fe2e098298a9414489de6e5eeb63b809ec7783e38177","src/diagnostic_arg.rs":"db4788b8b301024401b46f21eb4e29118e59ad9d6790bc0fdec897d1d0dfcffa","src/diagnostic_source.rs":"e6012c7f6b8154ac36006d2a10eddf1311c322e504a9534b870bedb0ce36bd5c","src/fmt.rs":"a6dff276ccdd09a19f9d04ad1a2f3ffa56872c402c94da57c9c8fb3bc891e70f","src/forward.rs":"b344a6a48a90cb9832b8ffa9ca0f913539b1df0c2d34c4975235c5dc39c4a23a","src/help.rs":"cb79fa64bd5e1f9c57ad7995614da1ef461298c20d8c962be4454e2c0f318b49","src/label.rs":"b16cd5a1e3cb9ae09ff28b28bf420e49b306029064411949db0b006b28f4e2a9","src/lib.rs":"d56635c4293a3f4a9264cec62706d2ae2fb4819dadaf598875baeabcdc47013d","src/related.rs":"113fb18b30ad2f05a3552fecb45f01b27bca569494fedfe9471f99a0467d7677","src/severity.rs":"00c2d86de650dde4809add940230eff7981dffea9151d08da377924e0f4cf7f2","src/source_code.rs":"ab68bd7dcdf5121403b3443d5741d770cd2b634de4b5011c1340eb173b42089a","src/url.rs":"4aacdfe28f9a1259ceb59368832c1a601906d64f268024d30856b6196c62aaeb","src/utils.rs":"249eb5284572fe97d24e0c1fff3009100a1fd2739414f70e4fe1542b73e8bbc2"}}
\ No newline at end of file
+{"package":null,"files":{".cargo-checksum.json":"0dc70120bfc5380119fdcb3079b533a3d2fd68ea6381c7496c4fcd3d460a669d","Android.bp":"29af978d63b6898e465b98ae30692c13c93258ec049a8c5913050a87c67e7601","Cargo.toml":"ac7e7f8878a251954538417da23110fcf4cbb6da1e0bc346c5ba18b7c2f5acdb","LICENSE":"5281d46044d26df9efdd5588d65aad4d49c29e46f7f720d727c9d33cd6b4644d","METADATA":"b32972e105176fe1ada6ec3f6d11f32b2547c0f7a826e62247cfd6d183441891","MODULE_LICENSE_APACHE2":"0d6f8afa3940b7f06bebee651376d43bc8b0d5b437337be2696d30377451e93a","cargo_embargo.json":"d17b8bd25b48dae259fb01e76a2efeb790fc436fe351c2ac40912800d1647777","src/code.rs":"ff1c86d9cbe87f54f0de905a5274b4c703a228168b64753a6bf3acfe1bd18356","src/diagnostic.rs":"04811edb4086d614f727fe2e098298a9414489de6e5eeb63b809ec7783e38177","src/diagnostic_arg.rs":"db4788b8b301024401b46f21eb4e29118e59ad9d6790bc0fdec897d1d0dfcffa","src/diagnostic_source.rs":"e6012c7f6b8154ac36006d2a10eddf1311c322e504a9534b870bedb0ce36bd5c","src/fmt.rs":"a6dff276ccdd09a19f9d04ad1a2f3ffa56872c402c94da57c9c8fb3bc891e70f","src/forward.rs":"b344a6a48a90cb9832b8ffa9ca0f913539b1df0c2d34c4975235c5dc39c4a23a","src/help.rs":"cb79fa64bd5e1f9c57ad7995614da1ef461298c20d8c962be4454e2c0f318b49","src/label.rs":"2170aade3cd887d81d472528bb8931542d0a2845c00dd559f30fdabb6a555402","src/lib.rs":"d56635c4293a3f4a9264cec62706d2ae2fb4819dadaf598875baeabcdc47013d","src/related.rs":"113fb18b30ad2f05a3552fecb45f01b27bca569494fedfe9471f99a0467d7677","src/severity.rs":"1f24380a911a143517cf48c170798dea1e36e56c32b4e2309b93840385df65c5","src/source_code.rs":"5593661da6ae95bf91b150de64963136cc19c05e0f736761012b225647a4259c","src/url.rs":"4aacdfe28f9a1259ceb59368832c1a601906d64f268024d30856b6196c62aaeb","src/utils.rs":"249eb5284572fe97d24e0c1fff3009100a1fd2739414f70e4fe1542b73e8bbc2"}}
\ No newline at end of file
diff --git a/crates/miette-derive/.cargo-checksum.json b/crates/miette-derive/.cargo-checksum.json
index 940faa0..c8d2556 100644
--- a/crates/miette-derive/.cargo-checksum.json
+++ b/crates/miette-derive/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"599ac29de32593292836994f2f10f64950344d3c052f142fa9d0512d652af53c","LICENSE":"610432849befa0e875d601d044e7e5441eb86c83d052681b23aef357b2a0b928","src/code.rs":"a66b16975c657e931bf650617c1652efed89710fdf7d8610f300f6538cdd9ad6","src/diagnostic.rs":"137e53721f10bf12f7bcd47dd699f3c7a44dad1fe7284b6924624c9853e96803","src/diagnostic_arg.rs":"6cc263379779b42e57152399bd8cc3ec872f7c047696705379cee9e147fc4156","src/diagnostic_source.rs":"e99b8decaa0d4dced2a0fd0667ebe348a6ff0169fc1298859c1a656d754ea86c","src/fmt.rs":"de956c7bdcf44458bb77d029c402e8aae3b97fc8de450710824e4b88f059229e","src/forward.rs":"778ab4ce5455efa9b9673ec83ddfae4b7064a4bf57a674a09fd40ef3b2ff2ff7","src/help.rs":"fa141050182100265174e40957d355dfa175d12822e6ef8ed2e5ba3e41abf991","src/label.rs":"762a5728672cd1ed1587bd048b43ea4ffd0974d49d976b3cf0f4282c8633f02a","src/lib.rs":"92aa427146667829d4faa2cf11da57defdedaa0f237384f3bfd7d8a0a593116f","src/related.rs":"fa6af1336fc6184f6727390757fa732d32cde249907fec56638f9a5f41bfcf45","src/severity.rs":"70fc6c592a0ad39fe9414b7115c7e0fb3e64eaa3ba5bef6b34ac35ad08ec6aa2","src/source_code.rs":"c3c8bf539dae2de0b0bc63ccf70dd4973b0516d9b95e9ac4190fa7468cce63de","src/url.rs":"b0ab232341844c0a3d604716624da8a9edcdb9cffbb188e008bfcc3f3046fe25","src/utils.rs":"db268d54ecc04bbba4cfb9b5a7b8b09922b2e4da396fdd47dd955b97a38740a2"},"package":"49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"}
\ No newline at end of file
+{"files":{"Cargo.toml":"5b5ab85a65d01ae008c2bdc6428570f34ca8be1a1c0e73a1c60514fcafb4605f","LICENSE":"02872c3ff23e11da5a9b3289df11b581be36b4956bfb7cfdc3d8043f92d64794","src/code.rs":"a66b16975c657e931bf650617c1652efed89710fdf7d8610f300f6538cdd9ad6","src/diagnostic.rs":"137e53721f10bf12f7bcd47dd699f3c7a44dad1fe7284b6924624c9853e96803","src/diagnostic_arg.rs":"6cc263379779b42e57152399bd8cc3ec872f7c047696705379cee9e147fc4156","src/diagnostic_source.rs":"e99b8decaa0d4dced2a0fd0667ebe348a6ff0169fc1298859c1a656d754ea86c","src/fmt.rs":"de956c7bdcf44458bb77d029c402e8aae3b97fc8de450710824e4b88f059229e","src/forward.rs":"778ab4ce5455efa9b9673ec83ddfae4b7064a4bf57a674a09fd40ef3b2ff2ff7","src/help.rs":"fa141050182100265174e40957d355dfa175d12822e6ef8ed2e5ba3e41abf991","src/label.rs":"5b93399ee4052339d742bad9ad5554a4ec73cedab0e44514fdbb90ee86a6f763","src/lib.rs":"92aa427146667829d4faa2cf11da57defdedaa0f237384f3bfd7d8a0a593116f","src/related.rs":"fa6af1336fc6184f6727390757fa732d32cde249907fec56638f9a5f41bfcf45","src/severity.rs":"7fdfbfd685a2b47eb8bd8b9175141a11caeb0d205242ec0c9b35168c2b537f65","src/source_code.rs":"a573a755faf895e9326f4ab5651a2e16182dbc98aab1ea649102f2d7ee9d27ed","src/url.rs":"b0ab232341844c0a3d604716624da8a9edcdb9cffbb188e008bfcc3f3046fe25","src/utils.rs":"db268d54ecc04bbba4cfb9b5a7b8b09922b2e4da396fdd47dd955b97a38740a2"},"package":"71e622f2a0dd84cbca79bc6c3c33f4fd7dc69faf992216516aacc1d136102800"}
\ No newline at end of file
diff --git a/crates/miette-derive/Android.bp b/crates/miette-derive/Android.bp
index 2fb4dea..ace989a 100644
--- a/crates/miette-derive/Android.bp
+++ b/crates/miette-derive/Android.bp
@@ -17,7 +17,7 @@
     name: "libmiette_derive",
     crate_name: "miette_derive",
     cargo_env_compat: true,
-    cargo_pkg_version: "5.10.0",
+    cargo_pkg_version: "6.0.1",
     crate_root: "src/lib.rs",
     edition: "2018",
     rustlibs: [
diff --git a/crates/miette-derive/Cargo.toml b/crates/miette-derive/Cargo.toml
index 84a9fe3..ce212a2 100644
--- a/crates/miette-derive/Cargo.toml
+++ b/crates/miette-derive/Cargo.toml
@@ -12,7 +12,7 @@
 [package]
 edition = "2018"
 name = "miette-derive"
-version = "5.10.0"
+version = "6.0.1"
 authors = ["Kat Marchán <kzm@zkat.tech>"]
 description = "Derive macros for miette. Like `thiserror` for Diagnostics."
 license = "Apache-2.0"
@@ -22,7 +22,7 @@
 proc-macro = true
 
 [dependencies.proc-macro2]
-version = "1.0"
+version = "1.0.60"
 
 [dependencies.quote]
 version = "1.0"
diff --git a/crates/miette-derive/LICENSE b/crates/miette-derive/LICENSE
index 10fa5cc..545f464 100644
--- a/crates/miette-derive/LICENSE
+++ b/crates/miette-derive/LICENSE
@@ -1,229 +1,229 @@
-Apache License

-

-Version 2.0, January 2004

-

-http://www.apache.org/licenses/ TERMS

-AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

-

-   1. Definitions.

-

-     

-

-

-      "License" shall mean the terms and conditions for use, reproduction, and

-distribution as defined by Sections 1 through 9 of this document.

-

-      

-

-     

-"Licensor" shall mean the copyright owner or entity authorized by the copyright

-owner that is granting the License.

-

-      

-

-      "Legal Entity" shall mean the

-union of the acting entity and all other entities that control, are controlled

-by, or are under common control with that entity. For the purposes of this

-definition, "control" means (i) the power, direct or indirect, to cause the

-direction or management of such entity, whether by contract or otherwise, or (ii)

-ownership of fifty percent (50%) or more of the outstanding shares, or (iii)

-beneficial ownership of such entity.

-

-      

-

-      "You" (or "Your") shall mean

-an individual or Legal Entity exercising permissions granted by this License.

-

-  

-

-

-      "Source" form shall mean the preferred form for making modifications,

-including but not limited to software source code, documentation source, and

-configuration files.

-

-      

-

-      "Object" form shall mean any form resulting

-from mechanical transformation or translation of a Source form, including but not

-limited to compiled object code, generated documentation, and conversions to

-other media types.

-

-      

-

-      "Work" shall mean the work of authorship,

-whether in Source or Object form, made available under the License, as indicated

-by a copyright notice that is included in or attached to the work (an example is

-provided in the Appendix below).

-

-      

-

-      "Derivative Works" shall mean any

-work, whether in Source or Object form, that is based on (or derived from) the

-Work and for which the editorial revisions, annotations, elaborations, or other

-modifications represent, as a whole, an original work of authorship. For the

-purposes of this License, Derivative Works shall not include works that remain

-separable from, or merely link (or bind by name) to the interfaces of, the Work

-and Derivative Works thereof.

-

-      

-

-      "Contribution" shall mean any work

-of authorship, including the original version of the Work and any modifications

-or additions to that Work or Derivative Works thereof, that is intentionally

-submitted to Licensor for inclusion in the Work by the copyright owner or by an

-individual or Legal Entity authorized to submit on behalf of the copyright owner.

-For the purposes of this definition, "submitted" means any form of electronic,

-verbal, or written communication sent to the Licensor or its representatives,

-including but not limited to communication on electronic mailing lists, source

-code control systems, and issue tracking systems that are managed by, or on

-behalf of, the Licensor for the purpose of discussing and improving the Work, but

-excluding communication that is conspicuously marked or otherwise designated in

-writing by the copyright owner as "Not a Contribution."

-

-      

-

-     

-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of

-whom a Contribution has been received by Licensor and subsequently incorporated

-within the Work.

-

-   2. Grant of Copyright License. Subject to the terms and

-conditions of this License, each Contributor hereby grants to You a perpetual,

-worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license

-to reproduce, prepare Derivative Works of, publicly display, publicly perform,

-sublicense, and distribute the Work and such Derivative Works in Source or Object

-form.

-

-   3. Grant of Patent License. Subject to the terms and conditions of this

-License, each Contributor hereby grants to You a perpetual, worldwide,

-non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this

-section) patent license to make, have made, use, offer to sell, sell, import, and

-otherwise transfer the Work, where such license applies only to those patent

-claims licensable by such Contributor that are necessarily infringed by their

-Contribution(s) alone or by combination of their Contribution(s) with the Work to

-which such Contribution(s) was submitted. If You institute patent litigation

-against any entity (including a cross-claim or counterclaim in a lawsuit)

-alleging that the Work or a Contribution incorporated within the Work constitutes

-direct or contributory patent infringement, then any patent licenses granted to

-You under this License for that Work shall terminate as of the date such

-litigation is filed.

-

-   4. Redistribution. You may reproduce and distribute

-copies of the Work or Derivative Works thereof in any medium, with or without

-modifications, and in Source or Object form, provided that You meet the following

-conditions:

-

-      (a) You must give any other recipients of the Work or

-Derivative Works a copy of this License; and

-

-      (b) You must cause any

-modified files to carry prominent notices stating that You changed the files;

-and

-

-      (c) You must retain, in the Source form of any Derivative Works that

-You distribute, all copyright, patent, trademark, and attribution notices from

-the Source form of the Work, excluding those notices that do not pertain to any

-part of the Derivative Works; and

-

-      (d) If the Work includes a "NOTICE" text

-file as part of its distribution, then any Derivative Works that You distribute

-must include a readable copy of the attribution notices contained within such

-NOTICE file, excluding those notices that do not pertain to any part of the

-Derivative Works, in at least one of the following places: within a NOTICE text

-file distributed as part of the Derivative Works; within the Source form or

-documentation, if provided along with the Derivative Works; or, within a display

-generated by the Derivative Works, if and wherever such third-party notices

-normally appear. The contents of the NOTICE file are for informational purposes

-only and do not modify the License. You may add Your own attribution notices

-within Derivative Works that You distribute, alongside or as an addendum to the

-NOTICE text from the Work, provided that such additional attribution notices

-cannot be construed as modifying the License.

-

-      You may add Your own

-copyright statement to Your modifications and may provide additional or different

-license terms and conditions for use, reproduction, or distribution of Your

-modifications, or for any such Derivative Works as a whole, provided Your use,

-reproduction, and distribution of the Work otherwise complies with the conditions

-stated in this License.

-

-   5. Submission of Contributions. Unless You explicitly

-state otherwise, any Contribution intentionally submitted for inclusion in the

-Work by You to the Licensor shall be under the terms and conditions of this

-License, without any additional terms or conditions. Notwithstanding the above,

-nothing herein shall supersede or modify the terms of any separate license

-agreement you may have executed with Licensor regarding such Contributions.

-

-  

-6. Trademarks. This License does not grant permission to use the trade names,

-trademarks, service marks, or product names of the Licensor, except as required

-for reasonable and customary use in describing the origin of the Work and

-reproducing the content of the NOTICE file.

-

-   7. Disclaimer of Warranty. Unless

-required by applicable law or agreed to in writing, Licensor provides the Work

-(and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT

-WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,

-without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,

-MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible

-for determining the appropriateness of using or redistributing the Work and

-assume any risks associated with Your exercise of permissions under this

-License.

-

-   8. Limitation of Liability. In no event and under no legal theory,

-whether in tort (including negligence), contract, or otherwise, unless required

-by applicable law (such as deliberate and grossly negligent acts) or agreed to in

-writing, shall any Contributor be liable to You for damages, including any

-direct, indirect, special, incidental, or consequential damages of any character

-arising as a result of this License or out of the use or inability to use the

-Work (including but not limited to damages for loss of goodwill, work stoppage,

-computer failure or malfunction, or any and all other commercial damages or

-losses), even if such Contributor has been advised of the possibility of such

-damages.

-

-   9. Accepting Warranty or Additional Liability. While redistributing

-the Work or Derivative Works thereof, You may choose to offer, and charge a fee

-for, acceptance of support, warranty, indemnity, or other liability obligations

-and/or rights consistent with this License. However, in accepting such

-obligations, You may act only on Your own behalf and on Your sole responsibility,

-not on behalf of any other Contributor, and only if You agree to indemnify,

-defend, and hold each Contributor harmless for any liability incurred by, or

-claims asserted against, such Contributor by reason of your accepting any such

-warranty or additional liability. END OF TERMS AND CONDITIONS

-

-APPENDIX: How to

-apply the Apache License to your work.

-

-To apply the Apache License to your work,

-attach the following boilerplate notice, with the fields enclosed by brackets

-"[]" replaced with your own identifying information. (Don't include the

-brackets!) The text should be enclosed in the appropriate comment syntax for the

-file format. We also recommend that a file or class name and description of

-purpose be included on the same "printed page" as the copyright notice for easier

-identification within third-party archives.

-

-Copyright [yyyy] Kat

-Marchán

-

-Licensed under the Apache License, Version 2.0 (the "License");

-

-you may

-not use this file except in compliance with the License.

-

-You may obtain a copy

-of the License at

-

-http://www.apache.org/licenses/LICENSE-2.0

-

-Unless required by

-applicable law or agreed to in writing, software

-

-distributed under the License

-is distributed on an "AS IS" BASIS,

-

-WITHOUT WARRANTIES OR CONDITIONS OF ANY

-KIND, either express or implied.

-

-See the License for the specific language

-governing permissions and

-

+Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/ TERMS
+AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+     
+
+
+      "License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+      
+
+     
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+      
+
+      "Legal Entity" shall mean the
+union of the acting entity and all other entities that control, are controlled
+by, or are under common control with that entity. For the purposes of this
+definition, "control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or otherwise, or (ii)
+ownership of fifty percent (50%) or more of the outstanding shares, or (iii)
+beneficial ownership of such entity.
+
+      
+
+      "You" (or "Your") shall mean
+an individual or Legal Entity exercising permissions granted by this License.
+
+  
+
+
+      "Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation source, and
+configuration files.
+
+      
+
+      "Object" form shall mean any form resulting
+from mechanical transformation or translation of a Source form, including but not
+limited to compiled object code, generated documentation, and conversions to
+other media types.
+
+      
+
+      "Work" shall mean the work of authorship,
+whether in Source or Object form, made available under the License, as indicated
+by a copyright notice that is included in or attached to the work (an example is
+provided in the Appendix below).
+
+      
+
+      "Derivative Works" shall mean any
+work, whether in Source or Object form, that is based on (or derived from) the
+Work and for which the editorial revisions, annotations, elaborations, or other
+modifications represent, as a whole, an original work of authorship. For the
+purposes of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of, the Work
+and Derivative Works thereof.
+
+      
+
+      "Contribution" shall mean any work
+of authorship, including the original version of the Work and any modifications
+or additions to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner or by an
+individual or Legal Entity authorized to submit on behalf of the copyright owner.
+For the purposes of this definition, "submitted" means any form of electronic,
+verbal, or written communication sent to the Licensor or its representatives,
+including but not limited to communication on electronic mailing lists, source
+code control systems, and issue tracking systems that are managed by, or on
+behalf of, the Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise designated in
+writing by the copyright owner as "Not a Contribution."
+
+      
+
+     
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of
+whom a Contribution has been received by Licensor and subsequently incorporated
+within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and
+conditions of this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license
+to reproduce, prepare Derivative Works of, publicly display, publicly perform,
+sublicense, and distribute the Work and such Derivative Works in Source or Object
+form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of this
+License, each Contributor hereby grants to You a perpetual, worldwide,
+non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell, import, and
+otherwise transfer the Work, where such license applies only to those patent
+claims licensable by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s) with the Work to
+which such Contribution(s) was submitted. If You institute patent litigation
+against any entity (including a cross-claim or counterclaim in a lawsuit)
+alleging that the Work or a Contribution incorporated within the Work constitutes
+direct or contributory patent infringement, then any patent licenses granted to
+You under this License for that Work shall terminate as of the date such
+litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute
+copies of the Work or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You meet the following
+conditions:
+
+      (a) You must give any other recipients of the Work or
+Derivative Works a copy of this License; and
+
+      (b) You must cause any
+modified files to carry prominent notices stating that You changed the files;
+and
+
+      (c) You must retain, in the Source form of any Derivative Works that
+You distribute, all copyright, patent, trademark, and attribution notices from
+the Source form of the Work, excluding those notices that do not pertain to any
+part of the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text
+file as part of its distribution, then any Derivative Works that You distribute
+must include a readable copy of the attribution notices contained within such
+NOTICE file, excluding those notices that do not pertain to any part of the
+Derivative Works, in at least one of the following places: within a NOTICE text
+file distributed as part of the Derivative Works; within the Source form or
+documentation, if provided along with the Derivative Works; or, within a display
+generated by the Derivative Works, if and wherever such third-party notices
+normally appear. The contents of the NOTICE file are for informational purposes
+only and do not modify the License. You may add Your own attribution notices
+within Derivative Works that You distribute, alongside or as an addendum to the
+NOTICE text from the Work, provided that such additional attribution notices
+cannot be construed as modifying the License.
+
+      You may add Your own
+copyright statement to Your modifications and may provide additional or different
+license terms and conditions for use, reproduction, or distribution of Your
+modifications, or for any such Derivative Works as a whole, provided Your use,
+reproduction, and distribution of the Work otherwise complies with the conditions
+stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly
+state otherwise, any Contribution intentionally submitted for inclusion in the
+Work by You to the Licensor shall be under the terms and conditions of this
+License, without any additional terms or conditions. Notwithstanding the above,
+nothing herein shall supersede or modify the terms of any separate license
+agreement you may have executed with Licensor regarding such Contributions.
+
+  
+6. Trademarks. This License does not grant permission to use the trade names,
+trademarks, service marks, or product names of the Licensor, except as required
+for reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless
+required by applicable law or agreed to in writing, Licensor provides the Work
+(and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
+without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
+MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible
+for determining the appropriateness of using or redistributing the Work and
+assume any risks associated with Your exercise of permissions under this
+License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise, unless required
+by applicable law (such as deliberate and grossly negligent acts) or agreed to in
+writing, shall any Contributor be liable to You for damages, including any
+direct, indirect, special, incidental, or consequential damages of any character
+arising as a result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill, work stoppage,
+computer failure or malfunction, or any and all other commercial damages or
+losses), even if such Contributor has been advised of the possibility of such
+damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer, and charge a fee
+for, acceptance of support, warranty, indemnity, or other liability obligations
+and/or rights consistent with this License. However, in accepting such
+obligations, You may act only on Your own behalf and on Your sole responsibility,
+not on behalf of any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability incurred by, or
+claims asserted against, such Contributor by reason of your accepting any such
+warranty or additional liability. END OF TERMS AND CONDITIONS
+
+APPENDIX: How to
+apply the Apache License to your work.
+
+To apply the Apache License to your work,
+attach the following boilerplate notice, with the fields enclosed by brackets
+"[]" replaced with your own identifying information. (Don't include the
+brackets!) The text should be enclosed in the appropriate comment syntax for the
+file format. We also recommend that a file or class name and description of
+purpose be included on the same "printed page" as the copyright notice for easier
+identification within third-party archives.
+
+Copyright [yyyy] Kat
+Marchán
+
+Licensed under the Apache License, Version 2.0 (the "License");
+
+you may
+not use this file except in compliance with the License.
+
+You may obtain a copy
+of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by
+applicable law or agreed to in writing, software
+
+distributed under the License
+is distributed on an "AS IS" BASIS,
+
+WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.
+
+See the License for the specific language
+governing permissions and
+
 limitations under the License.
\ No newline at end of file
diff --git a/crates/miette-derive/METADATA b/crates/miette-derive/METADATA
index 3756b57..c4f8881 100644
--- a/crates/miette-derive/METADATA
+++ b/crates/miette-derive/METADATA
@@ -1,17 +1,17 @@
 name: "miette-derive"
 description: "Derive macros for miette. Like `thiserror` for Diagnostics."
 third_party {
-  version: "5.10.0"
+  version: "6.0.1"
   license_type: NOTICE
   last_upgrade_date {
-    year: 2024
+    year: 2025
     month: 2
-    day: 2
+    day: 6
   }
   homepage: "https://crates.io/crates/miette-derive"
   identifier {
     type: "Archive"
-    value: "https://static.crates.io/crates/miette-derive/miette-derive-5.10.0.crate"
-    version: "5.10.0"
+    value: "https://static.crates.io/crates/miette-derive/miette-derive-6.0.1.crate"
+    version: "6.0.1"
   }
 }
diff --git a/crates/miette-derive/src/label.rs b/crates/miette-derive/src/label.rs
index e0bc70a..dd5ec69 100644
--- a/crates/miette-derive/src/label.rs
+++ b/crates/miette-derive/src/label.rs
@@ -20,10 +20,12 @@
     label: Option<Display>,
     ty: syn::Type,
     span: syn::Member,
+    primary: bool,
 }
 
 struct LabelAttr {
     label: Option<Display>,
+    primary: bool,
 }
 
 impl Parse for LabelAttr {
@@ -40,10 +42,22 @@
             }
         });
         let la = input.lookahead1();
-        let label = if la.peek(syn::token::Paren) {
-            // #[label("{}", x)]
+        let (primary, label) = if la.peek(syn::token::Paren) {
+            // #[label(primary?, "{}", x)]
             let content;
             parenthesized!(content in input);
+
+            let primary = if content.peek(syn::Ident) {
+                let ident: syn::Ident = content.parse()?;
+                if ident != "primary" {
+                    return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
+                }
+                let _ = content.parse::<Token![,]>();
+                true
+            } else {
+                false
+            };
+
             if content.peek(syn::LitStr) {
                 let fmt = content.parse()?;
                 let args = if content.is_empty() {
@@ -56,22 +70,27 @@
                     args,
                     has_bonus_display: false,
                 };
-                Some(display)
+                (primary, Some(display))
+            } else if !primary {
+                return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
             } else {
-                return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The first argument must be a literal string."));
+                (primary, None)
             }
         } else if la.peek(Token![=]) {
             // #[label = "blabla"]
             input.parse::<Token![=]>()?;
-            Some(Display {
-                fmt: input.parse()?,
-                args: TokenStream::new(),
-                has_bonus_display: false,
-            })
+            (
+                false,
+                Some(Display {
+                    fmt: input.parse()?,
+                    args: TokenStream::new(),
+                    has_bonus_display: false,
+                }),
+            )
         } else {
-            None
+            (false, None)
         };
-        Ok(LabelAttr { label })
+        Ok(LabelAttr { label, primary })
     }
 }
 
@@ -100,12 +119,21 @@
                         })
                     };
                     use quote::ToTokens;
-                    let LabelAttr { label } =
+                    let LabelAttr { label, primary } =
                         syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?;
+
+                    if primary && labels.iter().any(|l: &Label| l.primary) {
+                        return Err(syn::Error::new(
+                            field.span(),
+                            "Cannot have more than one primary label.",
+                        ));
+                    }
+
                     labels.push(Label {
                         label,
                         span,
                         ty: field.ty.clone(),
+                        primary,
                     });
                 }
             }
@@ -120,13 +148,23 @@
     pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
         let (display_pat, display_members) = display_pat_members(fields);
         let labels = self.0.iter().map(|highlight| {
-            let Label { span, label, ty } = highlight;
+            let Label {
+                span,
+                label,
+                ty,
+                primary,
+            } = highlight;
             let var = quote! { __miette_internal_var };
+            let ctor = if *primary {
+                quote! { miette::LabeledSpan::new_primary_with_span }
+            } else {
+                quote! { miette::LabeledSpan::new_with_span }
+            };
             if let Some(display) = label {
                 let (fmt, args) = display.expand_shorthand_cloned(&display_members);
                 quote! {
                     miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
-                    .map(|#var| miette::LabeledSpan::new_with_span(
+                    .map(|#var| #ctor(
                         std::option::Option::Some(format!(#fmt #args)),
                         #var.clone(),
                     ))
@@ -134,7 +172,7 @@
             } else {
                 quote! {
                     miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
-                    .map(|#var| miette::LabeledSpan::new_with_span(
+                    .map(|#var| #ctor(
                         std::option::Option::None,
                         #var.clone(),
                     ))
@@ -161,7 +199,7 @@
                 let (display_pat, display_members) = display_pat_members(fields);
                 labels.as_ref().and_then(|labels| {
                     let variant_labels = labels.0.iter().map(|label| {
-                        let Label { span, label, ty } = label;
+                        let Label { span, label, ty, primary } = label;
                         let field = match &span {
                             syn::Member::Named(ident) => ident.clone(),
                             syn::Member::Unnamed(syn::Index { index, .. }) => {
@@ -169,11 +207,16 @@
                             }
                         };
                         let var = quote! { __miette_internal_var };
+                        let ctor = if *primary {
+                            quote! { miette::LabeledSpan::new_primary_with_span }
+                        } else {
+                            quote! { miette::LabeledSpan::new_with_span }
+                        };
                         if let Some(display) = label {
                             let (fmt, args) = display.expand_shorthand_cloned(&display_members);
                             quote! {
                                 miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
-                                .map(|#var| miette::LabeledSpan::new_with_span(
+                                .map(|#var| #ctor(
                                     std::option::Option::Some(format!(#fmt #args)),
                                     #var.clone(),
                                 ))
@@ -181,7 +224,7 @@
                         } else {
                             quote! {
                                 miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
-                                .map(|#var| miette::LabeledSpan::new_with_span(
+                                .map(|#var| #ctor(
                                     std::option::Option::None,
                                     #var.clone(),
                                 ))
diff --git a/crates/miette-derive/src/severity.rs b/crates/miette-derive/src/severity.rs
index 76126de..4f26e4e 100644
--- a/crates/miette-derive/src/severity.rs
+++ b/crates/miette-derive/src/severity.rs
@@ -1,89 +1,89 @@
-use proc_macro2::{Span, TokenStream};

-use quote::quote;

-use syn::{

-    parenthesized,

-    parse::{Parse, ParseStream},

-    Token,

-};

-

-use crate::{

-    diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},

-    forward::WhichFn,

-    utils::gen_all_variants_with,

-};

-

-pub struct Severity(pub syn::Ident);

-

-impl Parse for Severity {

-    fn parse(input: ParseStream) -> syn::Result<Self> {

-        let ident = input.parse::<syn::Ident>()?;

-        if ident == "severity" {

-            let la = input.lookahead1();

-            if la.peek(syn::token::Paren) {

-                let content;

-                parenthesized!(content in input);

-                let la = content.lookahead1();

-                if la.peek(syn::LitStr) {

-                    let str = content.parse::<syn::LitStr>()?;

-                    let sev = get_severity(&str.value(), str.span())?;

-                    Ok(Severity(syn::Ident::new(&sev, str.span())))

-                } else {

-                    let ident = content.parse::<syn::Ident>()?;

-                    let sev = get_severity(&ident.to_string(), ident.span())?;

-                    Ok(Severity(syn::Ident::new(&sev, ident.span())))

-                }

-            } else {

-                input.parse::<Token![=]>()?;

-                let str = input.parse::<syn::LitStr>()?;

-                let sev = get_severity(&str.value(), str.span())?;

-                Ok(Severity(syn::Ident::new(&sev, str.span())))

-            }

-        } else {

-            Err(syn::Error::new(

-                ident.span(),

-                "MIETTE BUG: not a severity option",

-            ))

-        }

-    }

-}

-

-fn get_severity(input: &str, span: Span) -> syn::Result<String> {

-    match input.to_lowercase().as_ref() {

-        "error" | "err" => Ok("Error".into()),

-        "warning" | "warn" => Ok("Warning".into()),

-        "advice" | "adv" | "info" => Ok("Advice".into()),

-        _ => Err(syn::Error::new(

-            span,

-            "Invalid severity level. Only Error, Warning, and Advice are supported.",

-        )),

-    }

-}

-

-impl Severity {

-    pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {

-        gen_all_variants_with(

-            variants,

-            WhichFn::Severity,

-            |ident, fields, DiagnosticConcreteArgs { severity, .. }| {

-                let severity = &severity.as_ref()?.0;

-                let fields = match fields {

-                    syn::Fields::Named(_) => quote! { { .. } },

-                    syn::Fields::Unnamed(_) => quote! { (..) },

-                    syn::Fields::Unit => quote! {},

-                };

-                Some(

-                    quote! { Self::#ident #fields => std::option::Option::Some(miette::Severity::#severity), },

-                )

-            },

-        )

-    }

-

-    pub(crate) fn gen_struct(&self) -> Option<TokenStream> {

-        let sev = &self.0;

-        Some(quote! {

-            fn severity(&self) -> std::option::Option<miette::Severity> {

-                Some(miette::Severity::#sev)

-            }

-        })

-    }

-}

+use proc_macro2::{Span, TokenStream};
+use quote::quote;
+use syn::{
+    parenthesized,
+    parse::{Parse, ParseStream},
+    Token,
+};
+
+use crate::{
+    diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+    forward::WhichFn,
+    utils::gen_all_variants_with,
+};
+
+pub struct Severity(pub syn::Ident);
+
+impl Parse for Severity {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let ident = input.parse::<syn::Ident>()?;
+        if ident == "severity" {
+            let la = input.lookahead1();
+            if la.peek(syn::token::Paren) {
+                let content;
+                parenthesized!(content in input);
+                let la = content.lookahead1();
+                if la.peek(syn::LitStr) {
+                    let str = content.parse::<syn::LitStr>()?;
+                    let sev = get_severity(&str.value(), str.span())?;
+                    Ok(Severity(syn::Ident::new(&sev, str.span())))
+                } else {
+                    let ident = content.parse::<syn::Ident>()?;
+                    let sev = get_severity(&ident.to_string(), ident.span())?;
+                    Ok(Severity(syn::Ident::new(&sev, ident.span())))
+                }
+            } else {
+                input.parse::<Token![=]>()?;
+                let str = input.parse::<syn::LitStr>()?;
+                let sev = get_severity(&str.value(), str.span())?;
+                Ok(Severity(syn::Ident::new(&sev, str.span())))
+            }
+        } else {
+            Err(syn::Error::new(
+                ident.span(),
+                "MIETTE BUG: not a severity option",
+            ))
+        }
+    }
+}
+
+fn get_severity(input: &str, span: Span) -> syn::Result<String> {
+    match input.to_lowercase().as_ref() {
+        "error" | "err" => Ok("Error".into()),
+        "warning" | "warn" => Ok("Warning".into()),
+        "advice" | "adv" | "info" => Ok("Advice".into()),
+        _ => Err(syn::Error::new(
+            span,
+            "Invalid severity level. Only Error, Warning, and Advice are supported.",
+        )),
+    }
+}
+
+impl Severity {
+    pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
+        gen_all_variants_with(
+            variants,
+            WhichFn::Severity,
+            |ident, fields, DiagnosticConcreteArgs { severity, .. }| {
+                let severity = &severity.as_ref()?.0;
+                let fields = match fields {
+                    syn::Fields::Named(_) => quote! { { .. } },
+                    syn::Fields::Unnamed(_) => quote! { (..) },
+                    syn::Fields::Unit => quote! {},
+                };
+                Some(
+                    quote! { Self::#ident #fields => std::option::Option::Some(miette::Severity::#severity), },
+                )
+            },
+        )
+    }
+
+    pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
+        let sev = &self.0;
+        Some(quote! {
+            fn severity(&self) -> std::option::Option<miette::Severity> {
+                Some(miette::Severity::#sev)
+            }
+        })
+    }
+}
diff --git a/crates/miette-derive/src/source_code.rs b/crates/miette-derive/src/source_code.rs
index 62f28e7..e1b85ab 100644
--- a/crates/miette-derive/src/source_code.rs
+++ b/crates/miette-derive/src/source_code.rs
@@ -10,6 +10,7 @@
 
 pub struct SourceCode {
     source_code: syn::Member,
+    is_option: bool,
 }
 
 impl SourceCode {
@@ -27,6 +28,19 @@
         for (i, field) in fields.iter().enumerate() {
             for attr in &field.attrs {
                 if attr.path().is_ident("source_code") {
+                    let is_option = if let syn::Type::Path(syn::TypePath {
+                        path: syn::Path { segments, .. },
+                        ..
+                    }) = &field.ty
+                    {
+                        segments
+                            .last()
+                            .map(|seg| seg.ident == "Option")
+                            .unwrap_or(false)
+                    } else {
+                        false
+                    };
+
                     let source_code = if let Some(ident) = field.ident.clone() {
                         syn::Member::Named(ident)
                     } else {
@@ -35,7 +49,10 @@
                             span: field.span(),
                         })
                     };
-                    return Ok(Some(SourceCode { source_code }));
+                    return Ok(Some(SourceCode {
+                        source_code,
+                        is_option,
+                    }));
                 }
             }
         }
@@ -45,11 +62,21 @@
     pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
         let (display_pat, _display_members) = display_pat_members(fields);
         let src = &self.source_code;
+        let ret = if self.is_option {
+            quote! {
+                self.#src.as_ref().map(|s| s as _)
+            }
+        } else {
+            quote! {
+                Some(&self.#src)
+            }
+        };
+
         Some(quote! {
             #[allow(unused_variables)]
             fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> {
                 let Self #display_pat = self;
-                Some(&self.#src)
+                #ret
             }
         })
     }
@@ -68,10 +95,19 @@
                         }
                     };
                     let variant_name = ident.clone();
+                    let ret = if source_code.is_option {
+                        quote! {
+                            #field.as_ref().map(|s| s as _)
+                        }
+                    } else {
+                        quote! {
+                            std::option::Option::Some(#field)
+                        }
+                    };
                     match &fields {
                         syn::Fields::Unit => None,
                         _ => Some(quote! {
-                            Self::#variant_name #display_pat => std::option::Option::Some(#field),
+                            Self::#variant_name #display_pat => #ret,
                         }),
                     }
                 })
diff --git a/crates/miette/.android-checksum.json b/crates/miette/.android-checksum.json
index 0c22a45..eda9479 100644
--- a/crates/miette/.android-checksum.json
+++ b/crates/miette/.android-checksum.json
@@ -1 +1 @@
-{"package":null,"files":{".cargo-checksum.json":"e2ecacabd05956737a85e4e75ea661564d52d0471f7ed477fbd779dcca439840","Android.bp":"e4fea20a58db4ab9b85ffecf38879040e0e836bf71c04be1a765f335921399a4","CHANGELOG.md":"a87a64fbead27926a63900614e026eef7e70e25706d9deb2342c7e0d66ef486f","CODE_OF_CONDUCT.md":"a4bd04c90895c800d5662d75e618bace729c69ec2f922f45c9e60a4fc3e04e96","CONTRIBUTING.md":"c2aca4ba28d37a84e35806c4f0c329fb9518ff40906d73cebb947b495202ee53","Cargo.toml":"658e4b0ff1897af045e6d2f13670b961339df45cb2ca89183da669a5a755bdaa","LICENSE":"7c6512d88b3127990067585f24881ba1f182c5c49a04cb1975b226b7be95709e","METADATA":"5392b685ffc0ea84ebdeb3f19fc1b1b1bc461df53cac7efb365ba1d70a1595d5","MODULE_LICENSE_APACHE2":"0d6f8afa3940b7f06bebee651376d43bc8b0d5b437337be2696d30377451e93a","Makefile.toml":"e789bcdd20135e93c4532dce514faed0759635735485ca31ad3a2f02b747dd27","README.md":"00ca00514c70fcff5e39079c32c1873ae011150bd1fda4b1b9efdc22b1cbef01","README.tpl":"5697d6b6d27e4ddbb06bd91d6e23883302933a969d3b5573bac44b010b1202f3","android_config.toml":"0f390735e2ee04b3a9d1130bbaca72c29c58ad34322f9ed6b1a22a4763d6fd5f","cargo_embargo.json":"7191eedf1302c4727641965be2efc8ebb1baa210b39476f7d6efe67540763f54","cliff.toml":"9fec58f591ed928c5b77833616b3e18ee13ec0b4baabab7e5ffc3951aa50d4a4","clippy.toml":"27bfdcf0bdcd097497ac6f2250c60d5f0700bedf25a5534eb7dd82766e747f6a","rustfmt.toml":"38087783077635f0ecd4348f01553dfaf070519ad5a6cf0ff1cc32a900d8492b","src/chain.rs":"c193750bbb0deb1ce6bd711bd5c1a6edc97cf2421fd4eb54e1a65bed37917928","src/diagnostic_chain.rs":"aa2d4965f0e3e326d254d3bba384fe1b9492d504e77467a6adb5266053e57485","src/error.rs":"e6708edca2b43fb751cc191eb54ff83cc6ed4790d17fef032f97409c8b8d4605","src/eyreish/context.rs":"daa066a03c7609c7d62bddb047ea54cdc084258788247f143e04645efd217145","src/eyreish/error.rs":"f9b19f17dc4a82e9334f76f0d8846de3a1749b36172da60fe437c0eb95652588","src/eyreish/fmt.rs":"3a65c1dcf3cadb01b6609a4ae69998f95a917b73d26c0900b3fd262bcca1e5f9","src/eyreish/into_diagnostic.rs":"365fb842e4570630d9b612edc528bcf95df08aaec0dd8db50d2ebe2265f5fe75","src/eyreish/kind.rs":"64a031ebf0dc0de47696a229b9e045bb773396c08798583fd013a7aa551d859f","src/eyreish/macros.rs":"9caa49bfc6c9d034a552e5e468a66a62b8ac330b20e50b9e4f30a62d5634c269","src/eyreish/mod.rs":"9d66cc0179ad784aa0ed73bc7c841bbcbed94400b8a2a672c30b074a4680bc32","src/eyreish/ptr.rs":"793928e7b6f9fc5a636448059e94debdb2938c0a27636045b1b4bb1742a0a7e7","src/eyreish/wrapper.rs":"a9c698441a30c91b9b41eff0ebc208f5bd7d574d6f158d78f7cac728b16fd286","src/handler.rs":"fce9a98069ccf863f0c51324a96e9a24e36bec8beed76e6ee801c2ceb741f85b","src/handlers/debug.rs":"1839488cd311ac9499971bcc0b0cd0afdc6566669608e9958c6e80675406acdf","src/handlers/graphical.rs":"9f5cc44de562ea6f34b3a5b5c216677648eadd28030157f18251e7aac6b385c9","src/handlers/json.rs":"839bf02fe3df50c3c1a17802883e6ad5c31bac4911ac1202a38d26cb21d76f5d","src/handlers/mod.rs":"4ab817f09f1a85d35286910e0930a0ecc70969f8fbe61069b76ebf55264673ab","src/handlers/narratable.rs":"88a61e3cefa991700fcea5a7afacdab37dc4981fd67554db96e36f4c2169b791","src/handlers/theme.rs":"b8a29b97de147e180acbed9ae469854f1ff322aa33df4b399d000078a24fc275","src/lib.rs":"18fbc38c384561fc39b9babb1b75207bf525316e4230d55f7ab841bdec9b7ee1","src/macro_helpers.rs":"2a125e4b19c69a344affac9964f32d6c04c380bea50d4643e9973e2a7935382f","src/miette_diagnostic.rs":"6395030c60cee16f04ad471ddb9d77cab70a687594b30f95916af7463ab775f3","src/named_source.rs":"53782458b84e2424cced22842b087181d94fd2b8cbedf80d443182ee5ea46609","src/panic.rs":"49d2f5dcaf1308ef787b40e7174d80492a049f05ed52e379aee10b1bfb71470b","src/protocol.rs":"a7d9a9f36ce7bb22a9d893c482cb8fbb990855f3464bf0b6a897445cc4745d3e","src/source_impls.rs":"ec9e5689f088c960ba753d91568267a0bd1f29941d53990b5845ffd70b65681e"}}
\ No newline at end of file
+{"package":null,"files":{".cargo-checksum.json":"98c3eda8c26d80e1320080b55ccb10ed8b6436f4bb0b6221c0a6069a283d3f94","Android.bp":"2aa20c2a6dd4001ecca96fd5b58b8e0c7e0efd1e72de580f6499f3c24d539b7d","CHANGELOG.md":"65c6979bd66b80aa37756269e96dcc86cfff0100e5c034b2cd685dd5674e245a","CODE_OF_CONDUCT.md":"a4bd04c90895c800d5662d75e618bace729c69ec2f922f45c9e60a4fc3e04e96","CONTRIBUTING.md":"c2aca4ba28d37a84e35806c4f0c329fb9518ff40906d73cebb947b495202ee53","Cargo.toml":"ecf8546a7b5d78acf498f88f89d8e56438b9b508d3515555421d519af18d7597","LICENSE":"7c6512d88b3127990067585f24881ba1f182c5c49a04cb1975b226b7be95709e","METADATA":"18859bbb85332f892a9319cce94d9f263422c3ffba8a110a0b083f5bc328f5a5","MODULE_LICENSE_APACHE2":"0d6f8afa3940b7f06bebee651376d43bc8b0d5b437337be2696d30377451e93a","Makefile.toml":"e789bcdd20135e93c4532dce514faed0759635735485ca31ad3a2f02b747dd27","README.md":"631553a4ca4335fac29631fb1d132c76f4bda351affa89ca4fd892565366cee9","README.tpl":"5697d6b6d27e4ddbb06bd91d6e23883302933a969d3b5573bac44b010b1202f3","android_config.toml":"0f390735e2ee04b3a9d1130bbaca72c29c58ad34322f9ed6b1a22a4763d6fd5f","cargo_embargo.json":"7191eedf1302c4727641965be2efc8ebb1baa210b39476f7d6efe67540763f54","cliff.toml":"9422e51f9349abcfa54e6d9deb0622352a5d29041eb6d1329f5fa4c72da22e7d","clippy.toml":"74d6d343eeb86889927c2bf9290cbc67dc83f2e520519afae082da3dc8fda69c","rustfmt.toml":"b2f9d730640bc5428434fedcff3caf3e1675fd9f4b9ba5b78fc7a880c6f4e2a5","src/chain.rs":"c193750bbb0deb1ce6bd711bd5c1a6edc97cf2421fd4eb54e1a65bed37917928","src/diagnostic_chain.rs":"aa2d4965f0e3e326d254d3bba384fe1b9492d504e77467a6adb5266053e57485","src/error.rs":"3d717cf74c5b796b07f8a09192dda4abaa087915ee0d181733ec9678635e09f1","src/eyreish/context.rs":"daa066a03c7609c7d62bddb047ea54cdc084258788247f143e04645efd217145","src/eyreish/error.rs":"7ca2a397116b08953920e5ff3bd75e26d0aa1faf2cdb19c3ebdcd69af2026bd6","src/eyreish/fmt.rs":"3a65c1dcf3cadb01b6609a4ae69998f95a917b73d26c0900b3fd262bcca1e5f9","src/eyreish/into_diagnostic.rs":"365fb842e4570630d9b612edc528bcf95df08aaec0dd8db50d2ebe2265f5fe75","src/eyreish/kind.rs":"8acfce5634ac76d912bf1039d3f59f03fc59f4a76d6fb31378768eec2d220271","src/eyreish/macros.rs":"9caa49bfc6c9d034a552e5e468a66a62b8ac330b20e50b9e4f30a62d5634c269","src/eyreish/mod.rs":"fcb558244877211acc1e89e19682f726dfb4ffd3923c1b7bac25b9bb49fbce70","src/eyreish/ptr.rs":"793928e7b6f9fc5a636448059e94debdb2938c0a27636045b1b4bb1742a0a7e7","src/eyreish/wrapper.rs":"5c0a555aa3ae8b593012aad03bbf086715a6cd55f70168beeb691f1cb4c092d0","src/handler.rs":"aed1f872d61fd54c62457dfd3e18626fffae9a3ad1194752da33ee43c5ee5cb8","src/handlers/debug.rs":"1839488cd311ac9499971bcc0b0cd0afdc6566669608e9958c6e80675406acdf","src/handlers/graphical.rs":"c292e0177f37564b4b8f695b26265dfb88fd12b65f2cfb41eeea14aaba176ef1","src/handlers/json.rs":"aa20e185318125eea7abf6a26bc55953e833102ce8ace5f63243c5895139f679","src/handlers/mod.rs":"4ab817f09f1a85d35286910e0930a0ecc70969f8fbe61069b76ebf55264673ab","src/handlers/narratable.rs":"88a61e3cefa991700fcea5a7afacdab37dc4981fd67554db96e36f4c2169b791","src/handlers/theme.rs":"e42eadd69467f92271fc75a3204d39497bce98f196d448f308ed6582e8e1cb6c","src/highlighters/blank.rs":"70cfe07316e5c99cd05f8fb2071702129891270381df4cf9588194209513a34b","src/highlighters/mod.rs":"69d37156ca38f5f1ed757ca36bc471f53187451bf6a332119082f0f6c5bac2ef","src/highlighters/syntect.rs":"1996f20a367c4846d2e38d74f9eadf0c22f6d3c8aaab32eeac2004572fe69f39","src/lib.rs":"9515fd47618a74bb8e877e9311168576f502d456551fb666c700552e1904cd6f","src/macro_helpers.rs":"2a125e4b19c69a344affac9964f32d6c04c380bea50d4643e9973e2a7935382f","src/miette_diagnostic.rs":"adfa35237cdda9c071de07218f542926f47981732a7c044fc6fb78b433a3cf8e","src/named_source.rs":"7b29bce74d1459ff1dc0b84e1c3a7417eac72c497d03ec29d3159291c8169ca9","src/panic.rs":"49d2f5dcaf1308ef787b40e7174d80492a049f05ed52e379aee10b1bfb71470b","src/protocol.rs":"1e0ceeeae1e8f2ca1bf034a7e88c07b105f1a16179e5e2ce90d02545c8abd23f","src/source_impls.rs":"ec9e5689f088c960ba753d91568267a0bd1f29941d53990b5845ffd70b65681e"}}
\ No newline at end of file
diff --git a/crates/miette/.cargo-checksum.json b/crates/miette/.cargo-checksum.json
index 7d27774..68ae044 100644
--- a/crates/miette/.cargo-checksum.json
+++ b/crates/miette/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"CHANGELOG.md":"811bd1126957b274befd66a6e05e5d8dea7d18a7a4e31c2cb6dde86b9b22d0b7","CODE_OF_CONDUCT.md":"6c1484ec74fed9b28348a6c9f42b63c05b01cb515e2e96649a00eefd058570b7","CONTRIBUTING.md":"7093c0c1d9227e3ffd6bc3f6d46bad0e57f1f4f9fd1fb6297b2fd6410fb2582b","Cargo.toml":"3f053f8592b35ad1628219dee1019a76b846ab5cd39307e09e9b13d4d93da202","LICENSE":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","Makefile.toml":"05e22484f6aa57dcdd27258ea9feb9d81aeb908f8dfea88b3067d37da535da79","README.md":"90de9a829571ecc82bd44812da8e7964faac58206fc97746fad765e8fc484d4d","README.tpl":"821c9d642662efecf91f9ae8283d0e5f5546dcd2210fff8b842d2323bbde6b3f","cliff.toml":"81c34d677a357969145e5ebc0976609b47504eb3fcfe520b9551ef6d7ced80c6","clippy.toml":"41df2f641fd7b29e27bb24f8bdfe10c98cc9bd2b75c074216f55b6a7abdb8f54","rustfmt.toml":"2c1fd2b83aa781b21d1f1511a9b11f2142b999a58795bb72ce7760a8f388fa36","src/chain.rs":"7dec2683bd5b934e74c04644f342b6a7c8c98b488effd22313e18e34f3c3fcc5","src/diagnostic_chain.rs":"a970144f33423f702c9044286ff621d385fd2eaeac04f03a1a6e62da602ab2af","src/error.rs":"9a5de6ced7d1238b57c09cd4383ea08d5edd943898d21920f4285a6685da928e","src/eyreish/context.rs":"c4cef07841191850d0113ff89eeccf6777deaa4517d789098088e75c6ebc77f8","src/eyreish/error.rs":"2df6ef70efc7ec34542d75e72a7a1eca69126f7d568fc38c28c342b3bfe8692c","src/eyreish/fmt.rs":"90ead24156830fdc9b4c46e257f7712446e3e9c56c089c66cb79884a69692da7","src/eyreish/into_diagnostic.rs":"d4d736b0f9e30f1c59504f62bafd9c08aaef24777e9ed935ea1cecb755525f93","src/eyreish/kind.rs":"726796e888193d13de1c036828392ab829ffa0a65add1eed1b1f9cc087f4380f","src/eyreish/macros.rs":"44e43128876b772c62a984ad68cc78d688defd331f5188967e2414d36f79ec48","src/eyreish/mod.rs":"0ac54b8c66b4dc999a985834b8bc1065dd061adbafa6ad426e7149d0971985b6","src/eyreish/ptr.rs":"57a312ab811e0b1e2056c0bd441a06ae920d7e0c5dabe279cad456804f4a9081","src/eyreish/wrapper.rs":"15e81c9a8cc6875270c8e9e46df104a2f9f7addc60183f013d03155db9e58a1f","src/handler.rs":"e36a3d348b0403c2ba9c2adbb5d82aa224a5f347e9ccd830c51715b4151361b4","src/handlers/debug.rs":"bbaa035327b3b3a526ebf5259cfa6be65ec86731b9c0ce9ec7b6175156fa2039","src/handlers/graphical.rs":"72b6a7f7dbde3f1cb50f4774f5cae0d10d8e0d15a51a5ad186fc6a4d67efebdb","src/handlers/json.rs":"29cb4f9726ec7e38eb2a08d6eb4aaa05d317738f21db9b214468d4572e7a69db","src/handlers/mod.rs":"52a553957d79370baf31b0903ae9e932d3edf57869ad2cc3338b834dcab7c45e","src/handlers/narratable.rs":"126793bc006d8909ed960637602ccc2104e05b1f4379fb712f0b6a2034b67a85","src/handlers/theme.rs":"ba5358fb9a89fd1b700ba8a9a0e1d08d227a18d2a1ff990e275e10260f691730","src/lib.rs":"e2c6c3936fce991df0afd28ed468e89e95f4ea24bcd0d6e457ecb2bae5734941","src/macro_helpers.rs":"bb3c63b9cd0e700338d2ecdf57ba0a170dc51daf6bf62e7806f957c1dfd020fe","src/miette_diagnostic.rs":"5782f26494adc6da924de63b3a3f61a53c7636e8fd11b1aabeec3e352a339508","src/named_source.rs":"c00e59fc62c50149730a592a46c8e63c50f486c3ae3ff632550f8aa21dc1bc6b","src/panic.rs":"b1dd1f1fed023fd3ac6b4ff6ba6d3ae8058d481e7bd16c3888c05ce58a5cbe9a","src/protocol.rs":"4d26db6be245425262abd8f2e93774a4c90ca4dcafb34891943f612fb86ed7c3","src/source_impls.rs":"08e33b3a93f5c600a6454b2aa9d1e9c3dd09dfbef95afcb6407ab499c3d69704"},"package":"59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"}
\ No newline at end of file
+{"files":{"CHANGELOG.md":"9d7141568346cca8f5a8f6cfa13222d4942ae5a2470d426dee0c8bb799a673e0","CODE_OF_CONDUCT.md":"6c1484ec74fed9b28348a6c9f42b63c05b01cb515e2e96649a00eefd058570b7","CONTRIBUTING.md":"7093c0c1d9227e3ffd6bc3f6d46bad0e57f1f4f9fd1fb6297b2fd6410fb2582b","Cargo.toml":"98d4c0aba7983893b6deda47da8e0cfcbc0eadc5a76e312a2141636c8c1d54d4","LICENSE":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","Makefile.toml":"05e22484f6aa57dcdd27258ea9feb9d81aeb908f8dfea88b3067d37da535da79","README.md":"58ae3043357edfaa769d2f616833b139933d7a2677955c7eb8fd9dc682d45ce7","README.tpl":"821c9d642662efecf91f9ae8283d0e5f5546dcd2210fff8b842d2323bbde6b3f","cliff.toml":"bee8b26eb8f3f1241e039b89469cbccc5edd1fbe49024a40b42baaf8accf5920","clippy.toml":"f66b3abd56c83e02f1f5a257624c343a0a029439441e3d9417e825a2cf411db9","rustfmt.toml":"f1a22634bbffec62822faa208b352d302c19c2f70e29e57613b9df18d7c2fc36","src/chain.rs":"7dec2683bd5b934e74c04644f342b6a7c8c98b488effd22313e18e34f3c3fcc5","src/diagnostic_chain.rs":"a970144f33423f702c9044286ff621d385fd2eaeac04f03a1a6e62da602ab2af","src/error.rs":"33b256ac386b3c83d3c9749a4de40c217b92da8a1e1429bf27bb1bdbc37121e1","src/eyreish/context.rs":"c4cef07841191850d0113ff89eeccf6777deaa4517d789098088e75c6ebc77f8","src/eyreish/error.rs":"771480534996aedc6cefb4629d4921b139d7a070acf852c6e8fc7ec3d2c11085","src/eyreish/fmt.rs":"90ead24156830fdc9b4c46e257f7712446e3e9c56c089c66cb79884a69692da7","src/eyreish/into_diagnostic.rs":"d4d736b0f9e30f1c59504f62bafd9c08aaef24777e9ed935ea1cecb755525f93","src/eyreish/kind.rs":"0d4dd291809a8effe27f06a4b86c9c3630165fa656c2ba30ce74028358e0dd1c","src/eyreish/macros.rs":"44e43128876b772c62a984ad68cc78d688defd331f5188967e2414d36f79ec48","src/eyreish/mod.rs":"fd121fad03219f54b63ded70a25a9f219e3ec84482ae1334467babd6dc0ada55","src/eyreish/ptr.rs":"57a312ab811e0b1e2056c0bd441a06ae920d7e0c5dabe279cad456804f4a9081","src/eyreish/wrapper.rs":"32b052bb06f4c994635d1000a260a87fa0104541b67f8509ed433a39520ae700","src/handler.rs":"5f74e386d4a2bf47fdf2096138cb94f87c158867063551b99c202aa271261948","src/handlers/debug.rs":"bbaa035327b3b3a526ebf5259cfa6be65ec86731b9c0ce9ec7b6175156fa2039","src/handlers/graphical.rs":"c57b480b13c83c2bc537007f0cfde5c2c6ba2ad21934d15d611583faa1f142c7","src/handlers/json.rs":"f7f22ec628a825693e2042f02ca0f5fcf7b3f9d163c1217c5d1dc53e33a236ca","src/handlers/mod.rs":"52a553957d79370baf31b0903ae9e932d3edf57869ad2cc3338b834dcab7c45e","src/handlers/narratable.rs":"126793bc006d8909ed960637602ccc2104e05b1f4379fb712f0b6a2034b67a85","src/handlers/theme.rs":"74939baeec44d3f497e5e8eaf903c2b1209d7f10abb57b2e5a61d1f6c9d062e5","src/highlighters/blank.rs":"60d5d765ded99ea4c93cbb0d6c6cc2d1a4d10b3f5517481fcfd12f2f34f5ba99","src/highlighters/mod.rs":"79d6a6d6ee377adb3df56d42796f70c7d3fad4566c0476c0c8f47713e4526466","src/highlighters/syntect.rs":"54818065f3a4cb3e5fc2741dedcbb8d165b81bd1cde0a536deec7ad01c8e9cec","src/lib.rs":"a86eefd57ba55b8c185aa4811594946ba2b3fe6ba730b73fef438deca46b32ae","src/macro_helpers.rs":"bb3c63b9cd0e700338d2ecdf57ba0a170dc51daf6bf62e7806f957c1dfd020fe","src/miette_diagnostic.rs":"7def75be49385787d353aaa6773d9dc92eb94687b77fdbd1676e17fc247385aa","src/named_source.rs":"378fd71891cc659218e5c62d02994da00d3fa815c5237730f3bfb8487b01b06a","src/panic.rs":"b1dd1f1fed023fd3ac6b4ff6ba6d3ae8058d481e7bd16c3888c05ce58a5cbe9a","src/protocol.rs":"beae95370c8e309e3d1e56dc06db584cfaf18c0f68f52f4aef82720db6971d66","src/source_impls.rs":"08e33b3a93f5c600a6454b2aa9d1e9c3dd09dfbef95afcb6407ab499c3d69704"},"package":"337e1043bbc086dac9d9674983bef52ac991ce150e09b5b8e35c5a73dd83f66c"}
\ No newline at end of file
diff --git a/crates/miette/Android.bp b/crates/miette/Android.bp
index 4cf927f..25103f3 100644
--- a/crates/miette/Android.bp
+++ b/crates/miette/Android.bp
@@ -17,12 +17,15 @@
     name: "libmiette",
     crate_name: "miette",
     cargo_env_compat: true,
-    cargo_pkg_version: "5.10.0",
+    cargo_pkg_version: "6.0.1",
     crate_root: "src/lib.rs",
     edition: "2018",
-    features: ["default"],
+    features: [
+        "default",
+        "derive",
+        "miette-derive",
+    ],
     rustlibs: [
-        "libonce_cell",
         "libthiserror",
         "libunicode_width",
     ],
diff --git a/crates/miette/CHANGELOG.md b/crates/miette/CHANGELOG.md
index 955c27d..4de742a 100644
--- a/crates/miette/CHANGELOG.md
+++ b/crates/miette/CHANGELOG.md
@@ -1,5 +1,63 @@
 # `miette` Release Changelog
 
+<a name="6.0.1"></a>
+## 6.0.1 (2024-02-04)
+
+### Bug Fixes
+
+* **graphical:** oops. Fix theme issue ([8b46679c](https://github.com/zkat/miette/commit/8b46679c3647e1455d91b4c68743c619fb3f3eb3))
+* **fmt:** remove nightly-only fmt flags ([1fa7f524](https://github.com/zkat/miette/commit/1fa7f5241fb91d2e5bad9b0e26bcc7cd5f9011f1))
+* **highlighter:** ugh, missed another spot ([ab7c066e](https://github.com/zkat/miette/commit/ab7c066e7675d8c7ecb956000d278fc31f3bc6a1))
+
+<a name="6.0.0"></a>
+## 6.0.0 (2024-02-04)
+
+The long-awaited 6.0 release of `miette` is here, with TONS of goodies, not
+least of which is syntax highlighting support!
+
+It also comes with a few breaking changes so make sure to check below and
+update your code as needed!
+
+### Features
+
+* **labels:** Add support for primary label in specifying line/col information (#291) ([db0b7e40](https://github.com/zkat/miette/commit/db0b7e403a5ae52ae360991b6508490d8c579886))
+* **derive:** Allow optional sources in derive (#301) ([88d00e0e](https://github.com/zkat/miette/commit/88d00e0e20bf95e03b8f81dcd5adf38c917e190e))
+* **derive:** Make `miette-derive` be able to be turned off (#304) ([c7ba5b7e](https://github.com/zkat/miette/commit/c7ba5b7e52e05991cecd3ca925c710bbe49850b9))
+* **graphical:** Expose additional `textwrap` options (#321) ([fd77257c](https://github.com/zkat/miette/commit/fd77257cee0f5d03aa7dccb4ba8cbaa40c1a88c6))
+* **graphical:** support rendering labels that contain newlines (#318) ([865d67c8](https://github.com/zkat/miette/commit/865d67c8dda119ddd03ac43be22f4fa272a9f433))
+* **graphical:** Add `wrap_lines: bool` option allowing wrapping be disabled entirely (#328) ([b0744462](https://github.com/zkat/miette/commit/b0744462adbbfbb6d845f382db36be883c7f3c45))
+* **graphical:** render disjoint snippets separately for cleaner output (#324) ([19c22143](https://github.com/zkat/miette/commit/19c22143cb544616046784e35c5e78cc5b881289))
+* **deps:** Bump terminal-size to v0.3.0 (#308) ([c0a298e5](https://github.com/zkat/miette/commit/c0a298e5a8d699acf9fcd61b5d5fa4f6279a47ab))
+    * **BREAKING CHANGE**: This requires an MSRV bump to 1.70.0.
+* **source-code:** Don't override provided source code (#300) ([0d5c2ce7](https://github.com/zkat/miette/commit/0d5c2ce7536b0ea205346595d8a00d00bfb6cbd2))
+    * **BREAKING CHANGE**: Source code is no longer overridden if it was provided by the diagnostic's own `source_code()` impl.
+* **source:** use `usize` for length (#265) ([fad0e76a](https://github.com/zkat/miette/commit/fad0e76ad2e19d5cac13cf8324338aca0d623d93))
+    * **BREAKING CHANGE**: This changes `SourceSpan`'s length type to `usize`.
+* **source:** Allow inner source type of a NamedSource to be borrowed (#254) ([1df3b1a5](https://github.com/zkat/miette/commit/1df3b1a537f2e54cd40ec45f5cd851337a22e95a))
+    * **BREAKING CHANGE**: This makes the `NamedSource` type generic over its `Source` type, instead of boxing it.
+* **highlighting:** add syntax highlighting support with syntect crate (#313) ([e65d0a78](https://github.com/zkat/miette/commit/e65d0a78cc639653f061a45d8ce35b1a3551ade7))
+* **deps:** remove is-terminal dep in favor of `std::io::IsTerminal` ([e5c7ae46](https://github.com/zkat/miette/commit/e5c7ae469e40a8bc102e1fca3b8fd4b2ec137696))
+* **deps:** remove once_cell dep in favor of `std::sync::OnceLock` ([4c48584f](https://github.com/zkat/miette/commit/4c48584f304414c6924bede3308b455cfef60749))
+    * **BREAKING CHANGE**: This requires an MSRV bump to 1.70.0.
+* **deps:** bump some semver-breaking deps to newer versions ([29d000f2](https://github.com/zkat/miette/commit/29d000f201b259a056867a2876384f97653a6e9e))
+* **MSRV:** Actually bump the MSRV to 1.70.0 ([ab59a7bc](https://github.com/zkat/miette/commit/ab59a7bc9bceace5761a862ee2ebff3e5943b12f))
+
+### Bug Fixes
+
+* **misc:** Improve ci and fix clippy (#290) ([cc81382a](https://github.com/zkat/miette/commit/cc81382a6070dd226a20e4a39518d88e957ac0e1))
+* **tests:** Fix `cargo test` with default features. (#294) ([1f448e47](https://github.com/zkat/miette/commit/1f448e47751d0f914134b0e9138fdb1a5a95d55c))
+* **clippy:** Add missing semicolons where nothing is returned. (#293) ([06b34823](https://github.com/zkat/miette/commit/06b348230aaf153b8b050322f05e5d185351d2d1))
+* **graphical:** Extend error text span to whole code points (#312) ([a8b4ae01](https://github.com/zkat/miette/commit/a8b4ae012aa0cf03b53a18f013c2b3f76c5040e7))
+* **formatting:** Fix formatting bug when an empty span is not aligned to a char boundary (#314) ([3d6f903d](https://github.com/zkat/miette/commit/3d6f903df0e7c9d0eb9a1fdbbf0028bab5496429))
+* **docs:** add example to README and docs fixing #96 (#319) ([251d6d59](https://github.com/zkat/miette/commit/251d6d59292397458328ef57fb7957faedafd019))
+* **graphical:** rendering bug on small spans in large spans (#316) ([7ff4f874](https://github.com/zkat/miette/commit/7ff4f874d693a665af4df40f4e94505013e3e262))
+* **graphical:** render cause chains for inner errors (#330) ([cb2ae2e1](https://github.com/zkat/miette/commit/cb2ae2e18b446a5e90885faf8a30b5672c307df8))
+* **handler:** remove the two extra `is_terminal` sys call from `MietteHandlerOpts::build` (#325) ([f1dc89c0](https://github.com/zkat/miette/commit/f1dc89c07640445d224b61ef96c6b25fcdf62dee))
+
+### Documentation
+
+* **README:** Move import of `NamedResult` to where it is used (#309) ([d37ada87](https://github.com/zkat/miette/commit/d37ada876a5831d3f47622274e334c9a24aa5d2b))
+
 <a name="5.10.0"></a>
 ## 5.10.0 (2023-07-16)
 
diff --git a/crates/miette/Cargo.toml b/crates/miette/Cargo.toml
index 4c57331..9c6a3c5 100644
--- a/crates/miette/Cargo.toml
+++ b/crates/miette/Cargo.toml
@@ -11,9 +11,9 @@
 
 [package]
 edition = "2018"
-rust-version = "1.56.0"
+rust-version = "1.70.0"
 name = "miette"
-version = "5.10.0"
+version = "6.0.1"
 authors = ["Kat Marchán <kzm@zkat.tech>"]
 exclude = [
     "images/",
@@ -38,18 +38,12 @@
 version = "0.2.1"
 optional = true
 
-[dependencies.is-terminal]
-version = "0.4.0"
+[dependencies.miette-derive]
+version = "=6.0.1"
 optional = true
 
-[dependencies.miette-derive]
-version = "=5.10.0"
-
-[dependencies.once_cell]
-version = "1.8.0"
-
 [dependencies.owo-colors]
-version = "3.0.0"
+version = "3.4.0"
 optional = true
 
 [dependencies.serde]
@@ -58,23 +52,27 @@
 optional = true
 
 [dependencies.supports-color]
-version = "2.0.0"
+version = "3.0.0"
 optional = true
 
 [dependencies.supports-hyperlinks]
-version = "2.0.0"
+version = "3.0.0"
 optional = true
 
 [dependencies.supports-unicode]
-version = "2.0.0"
+version = "3.0.0"
+optional = true
+
+[dependencies.syntect]
+version = "5.1.0"
 optional = true
 
 [dependencies.terminal_size]
-version = "0.1.17"
+version = "0.3.0"
 optional = true
 
 [dependencies.textwrap]
-version = "0.15.0"
+version = "0.16.0"
 optional = true
 
 [dependencies.thiserror]
@@ -105,6 +103,9 @@
 [dev-dependencies.serde_json]
 version = "1.0.64"
 
+[dev-dependencies.strip-ansi-escapes]
+version = "0.2.0"
+
 [dev-dependencies.syn]
 version = "2.0"
 features = ["full"]
@@ -114,7 +115,8 @@
 features = ["diff"]
 
 [features]
-default = []
+default = ["derive"]
+derive = ["miette-derive"]
 fancy = [
     "fancy-no-backtrace",
     "backtrace",
@@ -122,7 +124,6 @@
 ]
 fancy-no-backtrace = [
     "owo-colors",
-    "is-terminal",
     "textwrap",
     "terminal_size",
     "supports-hyperlinks",
@@ -130,3 +131,7 @@
     "supports-unicode",
 ]
 no-format-args-capture = []
+syntect-highlighter = [
+    "fancy-no-backtrace",
+    "syntect",
+]
diff --git a/crates/miette/METADATA b/crates/miette/METADATA
index d75074a..c10974d 100644
--- a/crates/miette/METADATA
+++ b/crates/miette/METADATA
@@ -1,17 +1,17 @@
 name: "miette"
 description: "Fancy diagnostic reporting library and protocol for us mere mortals who aren\'t compiler hackers."
 third_party {
-  version: "5.10.0"
+  version: "6.0.1"
   license_type: NOTICE
   last_upgrade_date {
-    year: 2024
+    year: 2025
     month: 2
-    day: 2
+    day: 6
   }
   homepage: "https://crates.io/crates/miette"
   identifier {
     type: "Archive"
-    value: "https://static.crates.io/crates/miette/miette-5.10.0.crate"
-    version: "5.10.0"
+    value: "https://static.crates.io/crates/miette/miette-6.0.1.crate"
+    version: "6.0.1"
   }
 }
diff --git a/crates/miette/README.md b/crates/miette/README.md
index 7aabe99..0e936b3 100644
--- a/crates/miette/README.md
+++ b/crates/miette/README.md
@@ -48,6 +48,7 @@
   - [... delayed source code](#-delayed-source-code)
   - [... handler options](#-handler-options)
   - [... dynamic diagnostics](#-dynamic-diagnostics)
+  - [... syntax highlighting](#-syntax-highlighting)
 - [Acknowledgements](#acknowledgements)
 - [License](#license)
 
@@ -110,7 +111,7 @@
     // The Source that we're gonna be printing snippets out of.
     // This can be a String if you don't have or care about file names.
     #[source_code]
-    src: NamedSource,
+    src: NamedSource<String>,
     // Snippets and highlights can be included in the diagnostic!
     #[label("This bit here")]
     bad_bit: SourceSpan,
@@ -305,6 +306,23 @@
 miette = { version = "X.Y.Z", features = ["fancy"] }
 ```
 
+Another way to display a diagnostic is by printing them using the debug formatter.
+This is, in fact, what returning diagnostics from main ends up doing.
+To do it yourself, you can write the following:
+
+```rust
+use miette::{IntoDiagnostic, Result};
+use semver::Version;
+
+fn just_a_random_function() {
+    let version_result: Result<Version> = "1.2.x".parse().into_diagnostic();
+    match version_result {
+        Err(e) => println!("{:?}", e),
+        Ok(version) => println!("{}", version),
+    }
+}
+```
+
 #### ... diagnostic code URLs
 
 `miette` supports providing a URL for individual diagnostics. This URL will
@@ -594,6 +612,7 @@
             .unicode(false)
             .context_lines(3)
             .tab_width(4)
+            .break_words(true)
             .build(),
     )
 }))
@@ -624,6 +643,38 @@
 println!("{:?}", report)
 ```
 
+#### ... syntax highlighting
+
+`miette` can be configured to highlight syntax in source code snippets.
+
+<!-- TODO: screenshot goes here once default Theme is decided -->
+
+To use the built-in highlighting functionality, you must enable the
+`syntect-highlighter` crate feature. When this feature is enabled, `miette` will
+automatically use the [`syntect`] crate to highlight the `#[source_code]`
+field of your [`Diagnostic`].
+
+Syntax detection with [`syntect`] is handled by checking 2 methods on the [`SpanContents`] trait, in order:
+* [language()](SpanContents::language) - Provides the name of the language
+  as a string. For example `"Rust"` will indicate Rust syntax highlighting.
+  You can set the language of the [`SpanContents`] produced by a
+  [`NamedSource`] via the [`with_language`](NamedSource::with_language)
+  method.
+* [name()](SpanContents::name) - In the absence of an explicitly set
+  language, the name is assumed to contain a file name or file path.
+  The highlighter will check for a file extension at the end of the name and
+  try to guess the syntax from that.
+
+If you want to use a custom highlighter, you can provide a custom
+implementation of the [`Highlighter`](highlighters::Highlighter)
+trait to [`MietteHandlerOpts`] by calling the
+[`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting)
+method. See the [`highlighters`] module docs for more details.
+
+### MSRV
+
+This crate requires rustc 1.70.0 or later.
+
 ### Acknowledgements
 
 `miette` was not developed in a void. It owes enormous credit to various
diff --git a/crates/miette/cliff.toml b/crates/miette/cliff.toml
index 22a0b78..7117c52 100644
--- a/crates/miette/cliff.toml
+++ b/crates/miette/cliff.toml
@@ -1,62 +1,62 @@
-# configuration file for git-cliff (0.1.0)

-

-[changelog]

-# changelog header

-header = """

-# `miette` Release Changelog

-

-"""

-

-# template for the changelog body

-# https://tera.netlify.app/docs/#introduction

-body = """

-{% if version %}\

-<a name="{{ version }}"></a>

-## {{ version | replace(from="v", to="") }} ({{ timestamp | date(format="%Y-%m-%d") }})

-{% else %}\

-## Unreleased

-{% endif %}\

-{% for group, commits in commits | filter(attribute="scope") | group_by(attribute="group") %}

-### {{ group | upper_first }}

-{% for commit in commits %}

-{% if commit.scope %}\

-* **{{ commit.scope }}:** {{ commit.message }} ([{{ commit.id | truncate(length=8, end="") }}](https://github.com/zkat/miette/commit/{{ commit.id }}))

-{%- if commit.breaking %}

-    * **BREAKING CHANGE**: {{ commit.breaking_description }}

-{%- endif %}\

-{% endif %}\

-{% endfor %}

-{% endfor %}

-"""

-

-# remove the leading and trailing whitespace from the template

-trim = false

-

-# changelog footer

-# footer = """

-# <!-- generated by git-cliff -->

-# """

-

-[git]

-# allow only conventional commits

-# https://www.conventionalcommits.org

-conventional_commits = true

-# regex for parsing and grouping commits

-commit_parsers = [

-    { message = "^feat*", group = "Features"},

-    { message = "^fix*", group = "Bug Fixes"},

-    { message = "^doc*", group = "Documentation"},

-    { message = "^perf*", group = "Performance"},

-    { message = "^refactor*", group = "Refactor"},

-    { message = "^style*", group = "Styling"},

-    { message = "^test*", group = "Testing"},

-    { message = "^chore\\(release\\): prepare for*", skip = true},

-    { message = "^chore*", group = "Miscellaneous Tasks"},

-    { body = ".*security", group = "Security"},

-]

-# filter out the commits that are not matched by commit parsers

-filter_commits = true

-# glob pattern for matching git tags

-# tag_pattern = "v?[0-9]*"

-# regex for skipping tags

-# skip_tags = "v0.1.0-beta.1"

+# configuration file for git-cliff (0.1.0)
+
+[changelog]
+# changelog header
+header = """
+# `miette` Release Changelog
+
+"""
+
+# template for the changelog body
+# https://tera.netlify.app/docs/#introduction
+body = """
+{% if version %}\
+<a name="{{ version }}"></a>
+## {{ version | replace(from="v", to="") }} ({{ timestamp | date(format="%Y-%m-%d") }})
+{% else %}\
+## Unreleased
+{% endif %}\
+{% for group, commits in commits | filter(attribute="scope") | group_by(attribute="group") %}
+### {{ group | upper_first }}
+{% for commit in commits %}
+{% if commit.scope %}\
+* **{{ commit.scope }}:** {{ commit.message }} ([{{ commit.id | truncate(length=8, end="") }}](https://github.com/zkat/miette/commit/{{ commit.id }}))
+{%- if commit.breaking %}
+    * **BREAKING CHANGE**: {{ commit.breaking_description }}
+{%- endif %}\
+{% endif %}\
+{% endfor %}
+{% endfor %}
+"""
+
+# remove the leading and trailing whitespace from the template
+trim = false
+
+# changelog footer
+# footer = """
+# <!-- generated by git-cliff -->
+# """
+
+[git]
+# allow only conventional commits
+# https://www.conventionalcommits.org
+conventional_commits = true
+# regex for parsing and grouping commits
+commit_parsers = [
+    { message = "^feat*", group = "Features"},
+    { message = "^fix*", group = "Bug Fixes"},
+    { message = "^doc*", group = "Documentation"},
+    { message = "^perf*", group = "Performance"},
+    { message = "^refactor*", group = "Refactor"},
+    { message = "^style*", group = "Styling"},
+    { message = "^test*", group = "Testing"},
+    { message = "^chore\\(release\\): prepare for*", skip = true},
+    { message = "^chore*", group = "Miscellaneous Tasks"},
+    { body = ".*security", group = "Security"},
+]
+# filter out the commits that are not matched by commit parsers
+filter_commits = true
+# glob pattern for matching git tags
+# tag_pattern = "v?[0-9]*"
+# regex for skipping tags
+# skip_tags = "v0.1.0-beta.1"
diff --git a/crates/miette/clippy.toml b/crates/miette/clippy.toml
index 0d369b5..1645c19 100644
--- a/crates/miette/clippy.toml
+++ b/crates/miette/clippy.toml
@@ -1 +1 @@
-msrv = "1.56.0"
+msrv = "1.70.0"
diff --git a/crates/miette/rustfmt.toml b/crates/miette/rustfmt.toml
index 8f9ebdd..3a26366 100644
--- a/crates/miette/rustfmt.toml
+++ b/crates/miette/rustfmt.toml
@@ -1,3 +1 @@
 edition = "2021"
-wrap_comments = true
-format_code_in_doc_comments = true
diff --git a/crates/miette/src/error.rs b/crates/miette/src/error.rs
index 56041ca..4e57a78 100644
--- a/crates/miette/src/error.rs
+++ b/crates/miette/src/error.rs
@@ -1,27 +1,51 @@
-use std::io;
+use std::{fmt, io};
 
 use thiserror::Error;
 
-use crate::{self as miette, Diagnostic};
+use crate::Diagnostic;
 
 /**
 Error enum for miette. Used by certain operations in the protocol.
 */
-#[derive(Debug, Diagnostic, Error)]
+#[derive(Debug, Error)]
 pub enum MietteError {
     /// Wrapper around [`std::io::Error`]. This is returned when something went
     /// wrong while reading a [`SourceCode`](crate::SourceCode).
     #[error(transparent)]
-    #[diagnostic(code(miette::io_error), url(docsrs))]
     IoError(#[from] io::Error),
 
     /// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the
     /// bounds of a given [`SourceCode`](crate::SourceCode).
     #[error("The given offset is outside the bounds of its Source")]
-    #[diagnostic(
-        code(miette::span_out_of_bounds),
-        help("Double-check your spans. Do you have an off-by-one error?"),
-        url(docsrs)
-    )]
     OutOfBounds,
 }
+
+impl Diagnostic for MietteError {
+    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+        match self {
+            MietteError::IoError(_) => Some(Box::new("miette::io_error")),
+            MietteError::OutOfBounds => Some(Box::new("miette::span_out_of_bounds")),
+        }
+    }
+
+    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+        match self {
+            MietteError::IoError(_) => None,
+            MietteError::OutOfBounds => Some(Box::new(
+                "Double-check your spans. Do you have an off-by-one error?",
+            )),
+        }
+    }
+
+    fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+        let crate_version = env!("CARGO_PKG_VERSION");
+        let variant = match self {
+            MietteError::IoError(_) => "#variant.IoError",
+            MietteError::OutOfBounds => "#variant.OutOfBounds",
+        };
+        Some(Box::new(format!(
+            "https://docs.rs/miette/{}/miette/enum.MietteError.html{}",
+            crate_version, variant,
+        )))
+    }
+}
diff --git a/crates/miette/src/eyreish/error.rs b/crates/miette/src/eyreish/error.rs
index 6b0dc34..677f368 100644
--- a/crates/miette/src/eyreish/error.rs
+++ b/crates/miette/src/eyreish/error.rs
@@ -30,9 +30,9 @@
 
     /// Create a new error object from a printable error message.
     ///
-    /// If the argument implements std::error::Error, prefer `Report::new`
+    /// If the argument implements [`std::error::Error`], prefer `Report::new`
     /// instead which preserves the underlying error's cause chain and
-    /// backtrace. If the argument may or may not implement std::error::Error
+    /// backtrace. If the argument may or may not implement [`std::error::Error`]
     /// now or in the future, use `miette!(err)` which handles either way
     /// correctly.
     ///
@@ -206,7 +206,7 @@
     /// Create a new error from an error message to wrap the existing error.
     ///
     /// For attaching a higher level error message to a `Result` as it is
-    /// propagated, the [crate::WrapErr] extension trait may be more
+    /// propagated, the [`WrapErr`](crate::WrapErr) extension trait may be more
     /// convenient than this function.
     ///
     /// The primary reason to use `error.wrap_err(...)` instead of
@@ -233,7 +233,7 @@
         unsafe { Report::construct(error, vtable, handler) }
     }
 
-    /// Compatibility re-export of wrap_err for interop with `anyhow`
+    /// Compatibility re-export of `wrap_err` for interop with `anyhow`
     pub fn context<D>(self, msg: D) -> Self
     where
         D: Display + Send + Sync + 'static,
diff --git a/crates/miette/src/eyreish/kind.rs b/crates/miette/src/eyreish/kind.rs
index 4eb9fef..ce60b50 100644
--- a/crates/miette/src/eyreish/kind.rs
+++ b/crates/miette/src/eyreish/kind.rs
@@ -1,111 +1,111 @@
-#![allow(missing_debug_implementations, missing_docs)]

-// Tagged dispatch mechanism for resolving the behavior of `miette!($expr)`.

-//

-// When miette! is given a single expr argument to turn into miette::Report, we

-// want the resulting Report to pick up the input's implementation of source()

-// and backtrace() if it has a std::error::Error impl, otherwise require nothing

-// more than Display and Debug.

-//

-// Expressed in terms of specialization, we want something like:

-//

-//     trait EyreNew {

-//         fn new(self) -> Report;

-//     }

-//

-//     impl<T> EyreNew for T

-//     where

-//         T: Display + Debug + Send + Sync + 'static,

-//     {

-//         default fn new(self) -> Report {

-//             /* no std error impl */

-//         }

-//     }

-//

-//     impl<T> EyreNew for T

-//     where

-//         T: std::error::Error + Send + Sync + 'static,

-//     {

-//         fn new(self) -> Report {

-//             /* use std error's source() and backtrace() */

-//         }

-//     }

-//

-// Since specialization is not stable yet, instead we rely on autoref behavior

-// of method resolution to perform tagged dispatch. Here we have two traits

-// AdhocKind and TraitKind that both have an miette_kind() method. AdhocKind is

-// implemented whether or not the caller's type has a std error impl, while

-// TraitKind is implemented only when a std error impl does exist. The ambiguity

-// is resolved by AdhocKind requiring an extra autoref so that it has lower

-// precedence.

-//

-// The miette! macro will set up the call in this form:

-//

-//     #[allow(unused_imports)]

-//     use $crate::private::{AdhocKind, TraitKind};

-//     let error = $msg;

-//     (&error).miette_kind().new(error)

-

-use super::Report;

-use core::fmt::{Debug, Display};

-

-use crate::Diagnostic;

-

-pub struct Adhoc;

-

-pub trait AdhocKind: Sized {

-    #[inline]

-    fn miette_kind(&self) -> Adhoc {

-        Adhoc

-    }

-}

-

-impl<T> AdhocKind for &T where T: ?Sized + Display + Debug + Send + Sync + 'static {}

-

-impl Adhoc {

-    #[cfg_attr(track_caller, track_caller)]

-    pub fn new<M>(self, message: M) -> Report

-    where

-        M: Display + Debug + Send + Sync + 'static,

-    {

-        Report::from_adhoc(message)

-    }

-}

-

-pub struct Trait;

-

-pub trait TraitKind: Sized {

-    #[inline]

-    fn miette_kind(&self) -> Trait {

-        Trait

-    }

-}

-

-impl<E> TraitKind for E where E: Into<Report> {}

-

-impl Trait {

-    #[cfg_attr(track_caller, track_caller)]

-    pub fn new<E>(self, error: E) -> Report

-    where

-        E: Into<Report>,

-    {

-        error.into()

-    }

-}

-

-pub struct Boxed;

-

-pub trait BoxedKind: Sized {

-    #[inline]

-    fn miette_kind(&self) -> Boxed {

-        Boxed

-    }

-}

-

-impl BoxedKind for Box<dyn Diagnostic + Send + Sync> {}

-

-impl Boxed {

-    #[cfg_attr(track_caller, track_caller)]

-    pub fn new(self, error: Box<dyn Diagnostic + Send + Sync>) -> Report {

-        Report::from_boxed(error)

-    }

-}

+#![allow(missing_debug_implementations, missing_docs)]
+// Tagged dispatch mechanism for resolving the behavior of `miette!($expr)`.
+//
+// When miette! is given a single expr argument to turn into miette::Report, we
+// want the resulting Report to pick up the input's implementation of source()
+// and backtrace() if it has a std::error::Error impl, otherwise require nothing
+// more than Display and Debug.
+//
+// Expressed in terms of specialization, we want something like:
+//
+//     trait EyreNew {
+//         fn new(self) -> Report;
+//     }
+//
+//     impl<T> EyreNew for T
+//     where
+//         T: Display + Debug + Send + Sync + 'static,
+//     {
+//         default fn new(self) -> Report {
+//             /* no std error impl */
+//         }
+//     }
+//
+//     impl<T> EyreNew for T
+//     where
+//         T: std::error::Error + Send + Sync + 'static,
+//     {
+//         fn new(self) -> Report {
+//             /* use std error's source() and backtrace() */
+//         }
+//     }
+//
+// Since specialization is not stable yet, instead we rely on autoref behavior
+// of method resolution to perform tagged dispatch. Here we have two traits
+// AdhocKind and TraitKind that both have an miette_kind() method. AdhocKind is
+// implemented whether or not the caller's type has a std error impl, while
+// TraitKind is implemented only when a std error impl does exist. The ambiguity
+// is resolved by AdhocKind requiring an extra autoref so that it has lower
+// precedence.
+//
+// The miette! macro will set up the call in this form:
+//
+//     #[allow(unused_imports)]
+//     use $crate::private::{AdhocKind, TraitKind};
+//     let error = $msg;
+//     (&error).miette_kind().new(error)
+
+use super::Report;
+use core::fmt::{Debug, Display};
+
+use crate::Diagnostic;
+
+pub struct Adhoc;
+
+pub trait AdhocKind: Sized {
+    #[inline]
+    fn miette_kind(&self) -> Adhoc {
+        Adhoc
+    }
+}
+
+impl<T> AdhocKind for &T where T: ?Sized + Display + Debug + Send + Sync + 'static {}
+
+impl Adhoc {
+    #[cfg_attr(track_caller, track_caller)]
+    pub fn new<M>(self, message: M) -> Report
+    where
+        M: Display + Debug + Send + Sync + 'static,
+    {
+        Report::from_adhoc(message)
+    }
+}
+
+pub struct Trait;
+
+pub trait TraitKind: Sized {
+    #[inline]
+    fn miette_kind(&self) -> Trait {
+        Trait
+    }
+}
+
+impl<E> TraitKind for E where E: Into<Report> {}
+
+impl Trait {
+    #[cfg_attr(track_caller, track_caller)]
+    pub fn new<E>(self, error: E) -> Report
+    where
+        E: Into<Report>,
+    {
+        error.into()
+    }
+}
+
+pub struct Boxed;
+
+pub trait BoxedKind: Sized {
+    #[inline]
+    fn miette_kind(&self) -> Boxed {
+        Boxed
+    }
+}
+
+impl BoxedKind for Box<dyn Diagnostic + Send + Sync> {}
+
+impl Boxed {
+    #[cfg_attr(track_caller, track_caller)]
+    pub fn new(self, error: Box<dyn Diagnostic + Send + Sync>) -> Report {
+        Report::from_boxed(error)
+    }
+}
diff --git a/crates/miette/src/eyreish/mod.rs b/crates/miette/src/eyreish/mod.rs
index 0efceed..0dfd878 100644
--- a/crates/miette/src/eyreish/mod.rs
+++ b/crates/miette/src/eyreish/mod.rs
@@ -7,8 +7,7 @@
 use core::fmt::Display;
 
 use std::error::Error as StdError;
-
-use once_cell::sync::OnceCell;
+use std::sync::OnceLock;
 
 #[allow(unreachable_pub)]
 pub use into_diagnostic::*;
@@ -62,7 +61,7 @@
 pub type ErrorHook =
     Box<dyn Fn(&(dyn Diagnostic + 'static)) -> Box<dyn ReportHandler> + Sync + Send + 'static>;
 
-static HOOK: OnceCell<ErrorHook> = OnceCell::new();
+static HOOK: OnceLock<ErrorHook> = OnceLock::new();
 
 /// Error indicating that [`set_hook()`] was unable to install the provided
 /// [`ErrorHook`].
diff --git a/crates/miette/src/eyreish/wrapper.rs b/crates/miette/src/eyreish/wrapper.rs
index 91a5ef3..6e65eb7 100644
--- a/crates/miette/src/eyreish/wrapper.rs
+++ b/crates/miette/src/eyreish/wrapper.rs
@@ -163,7 +163,7 @@
     }
 
     fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-        Some(&self.source_code)
+        self.error.source_code().or(Some(&self.source_code))
     }
 
     fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
@@ -197,7 +197,7 @@
     }
 
     fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-        Some(&self.source_code)
+        self.error.source_code().or(Some(&self.source_code))
     }
 
     fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
@@ -232,3 +232,88 @@
         self.error.source()
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use thiserror::Error;
+
+    use crate::{Diagnostic, LabeledSpan, Report, SourceCode, SourceSpan};
+
+    #[derive(Error, Debug)]
+    #[error("inner")]
+    struct Inner {
+        pub(crate) at: SourceSpan,
+        pub(crate) source_code: Option<String>,
+    }
+
+    impl Diagnostic for Inner {
+        fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
+            Some(Box::new(std::iter::once(LabeledSpan::underline(self.at))))
+        }
+
+        fn source_code(&self) -> Option<&dyn SourceCode> {
+            self.source_code.as_ref().map(|s| s as _)
+        }
+    }
+
+    #[derive(Error, Debug)]
+    #[error("outer")]
+    struct Outer {
+        pub(crate) errors: Vec<Inner>,
+    }
+
+    impl Diagnostic for Outer {
+        fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
+            Some(Box::new(self.errors.iter().map(|e| e as _)))
+        }
+    }
+
+    #[test]
+    fn no_override() {
+        let inner_source = "hello world";
+        let outer_source = "abc";
+
+        let report = Report::from(Inner {
+            at: (0..5).into(),
+            source_code: Some(inner_source.to_string()),
+        })
+        .with_source_code(outer_source.to_string());
+
+        let underlined = String::from_utf8(
+            report
+                .source_code()
+                .unwrap()
+                .read_span(&(0..5).into(), 0, 0)
+                .unwrap()
+                .data()
+                .to_vec(),
+        )
+        .unwrap();
+        assert_eq!(underlined, "hello");
+    }
+
+    #[test]
+    #[cfg(feature = "fancy")]
+    fn two_source_codes() {
+        let inner_source = "hello world";
+        let outer_source = "abc";
+
+        let report = Report::from(Outer {
+            errors: vec![
+                Inner {
+                    at: (0..5).into(),
+                    source_code: Some(inner_source.to_string()),
+                },
+                Inner {
+                    at: (1..2).into(),
+                    source_code: None,
+                },
+            ],
+        })
+        .with_source_code(outer_source.to_string());
+
+        let message = format!("{:?}", report);
+        assert!(message.contains(inner_source));
+        assert!(message.contains(outer_source));
+    }
+}
diff --git a/crates/miette/src/handler.rs b/crates/miette/src/handler.rs
index e983a55..dcf8b13 100644
--- a/crates/miette/src/handler.rs
+++ b/crates/miette/src/handler.rs
@@ -1,5 +1,7 @@
 use std::fmt;
 
+use crate::highlighters::Highlighter;
+use crate::highlighters::MietteHighlighter;
 use crate::protocol::Diagnostic;
 use crate::GraphicalReportHandler;
 use crate::GraphicalTheme;
@@ -55,6 +57,11 @@
     pub(crate) context_lines: Option<usize>,
     pub(crate) tab_width: Option<usize>,
     pub(crate) with_cause_chain: Option<bool>,
+    pub(crate) break_words: Option<bool>,
+    pub(crate) wrap_lines: Option<bool>,
+    pub(crate) word_separator: Option<textwrap::WordSeparator>,
+    pub(crate) word_splitter: Option<textwrap::WordSplitter>,
+    pub(crate) highlighter: Option<MietteHighlighter>,
 }
 
 impl MietteHandlerOpts {
@@ -80,12 +87,79 @@
         self
     }
 
+    /// Set a syntax highlighter when rendering in graphical mode.
+    /// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
+    /// force graphical mode.
+    ///
+    /// Syntax highlighting is disabled by default unless the
+    /// `syntect-highlighter` feature is enabled. Call this method
+    /// to override the default and use a custom highlighter
+    /// implmentation instead.
+    ///
+    /// Use
+    /// [`without_syntax_highlighting()`](MietteHandlerOpts::without_syntax_highlighting())
+    /// To disable highlighting completely.
+    ///
+    /// Setting this option will not force color output. In all cases, the
+    /// current color configuration via
+    /// [`color()`](MietteHandlerOpts::color()) takes precedence over
+    /// highlighter configuration.
+    pub fn with_syntax_highlighting(
+        mut self,
+        highlighter: impl Highlighter + Send + Sync + 'static,
+    ) -> Self {
+        self.highlighter = Some(MietteHighlighter::from(highlighter));
+        self
+    }
+
+    /// Disables syntax highlighting when rendering in graphical mode.
+    /// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
+    /// force graphical mode.
+    ///
+    /// Syntax highlighting is disabled by default unless the
+    /// `syntect-highlighter` feature is enabled. Call this method if you want
+    /// to disable highlighting when building with this feature.
+    pub fn without_syntax_highlighting(mut self) -> Self {
+        self.highlighter = Some(MietteHighlighter::nocolor());
+        self
+    }
+
     /// Sets the width to wrap the report at. Defaults to 80.
     pub fn width(mut self, width: usize) -> Self {
         self.width = Some(width);
         self
     }
 
+    /// If true, long lines can be wrapped.
+    ///
+    /// If false, long lines will not be broken when they exceed the width.
+    ///
+    /// Defaults to true.
+    pub fn wrap_lines(mut self, wrap_lines: bool) -> Self {
+        self.wrap_lines = Some(wrap_lines);
+        self
+    }
+
+    /// If true, long words can be broken when wrapping.
+    ///
+    /// If false, long words will not be broken when they exceed the width.
+    ///
+    /// Defaults to true.
+    pub fn break_words(mut self, break_words: bool) -> Self {
+        self.break_words = Some(break_words);
+        self
+    }
+    /// Sets the `textwrap::WordSeparator` to use when determining wrap points.
+    pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
+        self.word_separator = Some(word_separator);
+        self
+    }
+
+    /// Sets the `textwrap::WordSplitter` to use when determining wrap points.
+    pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
+        self.word_splitter = Some(word_splitter);
+        self
+    }
     /// Include the cause chain of the top-level error in the report.
     pub fn with_cause_chain(mut self) -> Self {
         self.with_cause_chain = Some(true);
@@ -212,11 +286,33 @@
             } else {
                 ThemeStyles::none()
             };
+            #[cfg(not(feature = "syntect-highlighter"))]
+            let highlighter = self.highlighter.unwrap_or_else(MietteHighlighter::nocolor);
+            #[cfg(feature = "syntect-highlighter")]
+            let highlighter = if self.color == Some(false) {
+                MietteHighlighter::nocolor()
+            } else if self.color == Some(true)
+                || supports_color::on(supports_color::Stream::Stderr).is_some()
+            {
+                match self.highlighter {
+                    Some(highlighter) => highlighter,
+                    None => match self.rgb_colors {
+                        // Because the syntect highlighter currently only supports 24-bit truecolor,
+                        // respect RgbColor::Never by disabling the highlighter.
+                        // TODO: In the future, find a way to convert the RGB syntect theme
+                        // into an ANSI color theme.
+                        RgbColors::Never => MietteHighlighter::nocolor(),
+                        _ => MietteHighlighter::syntect_truecolor(),
+                    },
+                }
+            } else {
+                MietteHighlighter::nocolor()
+            };
             let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles });
-            let mut handler = GraphicalReportHandler::new()
+            let mut handler = GraphicalReportHandler::new_themed(theme)
                 .with_width(width)
-                .with_links(linkify)
-                .with_theme(theme);
+                .with_links(linkify);
+            handler.highlighter = highlighter;
             if let Some(with_cause_chain) = self.with_cause_chain {
                 if with_cause_chain {
                     handler = handler.with_cause_chain();
@@ -233,6 +329,19 @@
             if let Some(w) = self.tab_width {
                 handler = handler.tab_width(w);
             }
+            if let Some(b) = self.break_words {
+                handler = handler.with_break_words(b)
+            }
+            if let Some(b) = self.wrap_lines {
+                handler = handler.with_wrap_lines(b)
+            }
+            if let Some(s) = self.word_separator {
+                handler = handler.with_word_separator(s)
+            }
+            if let Some(s) = self.word_splitter {
+                handler = handler.with_word_splitter(s)
+            }
+
             MietteHandler {
                 inner: Box::new(handler),
             }
diff --git a/crates/miette/src/handlers/graphical.rs b/crates/miette/src/handlers/graphical.rs
index b5dd754..d7e1883 100644
--- a/crates/miette/src/handlers/graphical.rs
+++ b/crates/miette/src/handlers/graphical.rs
@@ -1,12 +1,13 @@
 use std::fmt::{self, Write};
 
-use owo_colors::{OwoColorize, Style};
+use owo_colors::{OwoColorize, Style, StyledList};
 use unicode_width::UnicodeWidthChar;
 
 use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
 use crate::handlers::theme::*;
+use crate::highlighters::{Highlighter, MietteHighlighter};
 use crate::protocol::{Diagnostic, Severity};
-use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
+use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents};
 
 /**
 A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a
@@ -30,6 +31,11 @@
     pub(crate) context_lines: usize,
     pub(crate) tab_width: usize,
     pub(crate) with_cause_chain: bool,
+    pub(crate) wrap_lines: bool,
+    pub(crate) break_words: bool,
+    pub(crate) word_separator: Option<textwrap::WordSeparator>,
+    pub(crate) word_splitter: Option<textwrap::WordSplitter>,
+    pub(crate) highlighter: MietteHighlighter,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -51,6 +57,11 @@
             context_lines: 1,
             tab_width: 4,
             with_cause_chain: true,
+            wrap_lines: true,
+            break_words: true,
+            word_separator: None,
+            word_splitter: None,
+            highlighter: MietteHighlighter::default(),
         }
     }
 
@@ -63,7 +74,12 @@
             footer: None,
             context_lines: 1,
             tab_width: 4,
+            wrap_lines: true,
             with_cause_chain: true,
+            break_words: true,
+            word_separator: None,
+            word_splitter: None,
+            highlighter: MietteHighlighter::default(),
         }
     }
 
@@ -122,6 +138,30 @@
         self
     }
 
+    /// Enables or disables wrapping of lines to fit the width.
+    pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
+        self.wrap_lines = wrap_lines;
+        self
+    }
+
+    /// Enables or disables breaking of words during wrapping.
+    pub fn with_break_words(mut self, break_words: bool) -> Self {
+        self.break_words = break_words;
+        self
+    }
+
+    /// Sets the word separator to use when wrapping.
+    pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
+        self.word_separator = Some(word_separator);
+        self
+    }
+
+    /// Sets the word splitter to usewhen wrapping.
+    pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
+        self.word_splitter = Some(word_splitter);
+        self
+    }
+
     /// Sets the 'global' footer for this handler.
     pub fn with_footer(mut self, footer: String) -> Self {
         self.footer = Some(footer);
@@ -133,6 +173,23 @@
         self.context_lines = lines;
         self
     }
+
+    /// Enable syntax highlighting for source code snippets, using the given
+    /// [`Highlighter`]. See the [crate::highlighters] crate for more details.
+    pub fn with_syntax_highlighting(
+        mut self,
+        highlighter: impl Highlighter + Send + Sync + 'static,
+    ) -> Self {
+        self.highlighter = MietteHighlighter::from(highlighter);
+        self
+    }
+
+    /// Disable syntax highlighting. This uses the
+    /// [`crate::highlighters::BlankHighlighter`] as a no-op highlighter.
+    pub fn without_syntax_highlighting(mut self) -> Self {
+        self.highlighter = MietteHighlighter::nocolor();
+        self
+    }
 }
 
 impl Default for GraphicalReportHandler {
@@ -159,10 +216,18 @@
         if let Some(footer) = &self.footer {
             writeln!(f)?;
             let width = self.termwidth.saturating_sub(4);
-            let opts = textwrap::Options::new(width)
+            let mut opts = textwrap::Options::new(width)
                 .initial_indent("  ")
-                .subsequent_indent("  ");
-            writeln!(f, "{}", textwrap::fill(footer, opts))?;
+                .subsequent_indent("  ")
+                .break_words(self.break_words);
+            if let Some(word_separator) = self.word_separator {
+                opts = opts.word_separator(word_separator);
+            }
+            if let Some(word_splitter) = self.word_splitter.clone() {
+                opts = opts.word_splitter(word_splitter);
+            }
+
+            writeln!(f, "{}", self.wrap(footer, opts))?;
         }
         Ok(())
     }
@@ -212,11 +277,18 @@
         let initial_indent = format!("  {} ", severity_icon.style(severity_style));
         let rest_indent = format!("  {} ", self.theme.characters.vbar.style(severity_style));
         let width = self.termwidth.saturating_sub(2);
-        let opts = textwrap::Options::new(width)
+        let mut opts = textwrap::Options::new(width)
             .initial_indent(&initial_indent)
-            .subsequent_indent(&rest_indent);
+            .subsequent_indent(&rest_indent)
+            .break_words(self.break_words);
+        if let Some(word_separator) = self.word_separator {
+            opts = opts.word_separator(word_separator);
+        }
+        if let Some(word_splitter) = self.word_splitter.clone() {
+            opts = opts.word_splitter(word_splitter);
+        }
 
-        writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
+        writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?;
 
         if !self.with_cause_chain {
             return Ok(());
@@ -251,23 +323,32 @@
                 )
                 .style(severity_style)
                 .to_string();
-                let opts = textwrap::Options::new(width)
+                let mut opts = textwrap::Options::new(width)
                     .initial_indent(&initial_indent)
-                    .subsequent_indent(&rest_indent);
+                    .subsequent_indent(&rest_indent)
+                    .break_words(self.break_words);
+                if let Some(word_separator) = self.word_separator {
+                    opts = opts.word_separator(word_separator);
+                }
+                if let Some(word_splitter) = self.word_splitter.clone() {
+                    opts = opts.word_splitter(word_splitter);
+                }
+
                 match error {
                     ErrorKind::Diagnostic(diag) => {
                         let mut inner = String::new();
 
-                        // Don't print footer for inner errors
                         let mut inner_renderer = self.clone();
+                        // Don't print footer for inner errors
                         inner_renderer.footer = None;
+                        // Cause chains are already flattened, so don't double-print the nested error
                         inner_renderer.with_cause_chain = false;
                         inner_renderer.render_report(&mut inner, diag)?;
 
-                        writeln!(f, "{}", textwrap::fill(&inner, opts))?;
+                        writeln!(f, "{}", self.wrap(&inner, opts))?;
                     }
                     ErrorKind::StdError(err) => {
-                        writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?;
+                        writeln!(f, "{}", self.wrap(&err.to_string(), opts))?;
                     }
                 }
             }
@@ -280,10 +361,18 @@
         if let Some(help) = diagnostic.help() {
             let width = self.termwidth.saturating_sub(4);
             let initial_indent = "  help: ".style(self.theme.styles.help).to_string();
-            let opts = textwrap::Options::new(width)
+            let mut opts = textwrap::Options::new(width)
                 .initial_indent(&initial_indent)
-                .subsequent_indent("        ");
-            writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
+                .subsequent_indent("        ")
+                .break_words(self.break_words);
+            if let Some(word_separator) = self.word_separator {
+                opts = opts.word_separator(word_separator);
+            }
+            if let Some(word_splitter) = self.word_splitter.clone() {
+                opts = opts.word_splitter(word_splitter);
+            }
+
+            writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
         }
         Ok(())
     }
@@ -295,6 +384,9 @@
         parent_src: Option<&dyn SourceCode>,
     ) -> fmt::Result {
         if let Some(related) = diagnostic.related() {
+            let mut inner_renderer = self.clone();
+            // Re-enable the printing of nested cause chains for related errors
+            inner_renderer.with_cause_chain = true;
             writeln!(f)?;
             for rel in related {
                 match rel.severity() {
@@ -302,12 +394,12 @@
                     Some(Severity::Warning) => write!(f, "Warning: ")?,
                     Some(Severity::Advice) => write!(f, "Advice: ")?,
                 };
-                self.render_header(f, rel)?;
-                self.render_causes(f, rel)?;
+                inner_renderer.render_header(f, rel)?;
+                inner_renderer.render_causes(f, rel)?;
                 let src = rel.source_code().or(parent_src);
-                self.render_snippets(f, rel, src)?;
-                self.render_footer(f, rel)?;
-                self.render_related(f, rel, src)?;
+                inner_renderer.render_snippets(f, rel, src)?;
+                inner_renderer.render_footer(f, rel)?;
+                inner_renderer.render_related(f, rel, src)?;
             }
         }
         Ok(())
@@ -319,78 +411,81 @@
         diagnostic: &(dyn Diagnostic),
         opt_source: Option<&dyn SourceCode>,
     ) -> fmt::Result {
-        if let Some(source) = opt_source {
-            if let Some(labels) = diagnostic.labels() {
-                let mut labels = labels.collect::<Vec<_>>();
-                labels.sort_unstable_by_key(|l| l.inner().offset());
-                if !labels.is_empty() {
-                    let contents = labels
-                        .iter()
-                        .map(|label| {
-                            source.read_span(label.inner(), self.context_lines, self.context_lines)
-                        })
-                        .collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>()
-                        .map_err(|_| fmt::Error)?;
-                    let mut contexts = Vec::with_capacity(contents.len());
-                    for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) {
-                        if contexts.is_empty() {
-                            contexts.push((right, right_conts));
-                        } else {
-                            let (left, left_conts) = contexts.last().unwrap().clone();
-                            let left_end = left.offset() + left.len();
-                            let right_end = right.offset() + right.len();
-                            if left_conts.line() + left_conts.line_count() >= right_conts.line() {
-                                // The snippets will overlap, so we create one Big Chunky Boi
-                                let new_span = LabeledSpan::new(
-                                    left.label().map(String::from),
-                                    left.offset(),
-                                    if right_end >= left_end {
-                                        // Right end goes past left end
-                                        right_end - left.offset()
-                                    } else {
-                                        // right is contained inside left
-                                        left.len()
-                                    },
-                                );
-                                if source
-                                    .read_span(
-                                        new_span.inner(),
-                                        self.context_lines,
-                                        self.context_lines,
-                                    )
-                                    .is_ok()
-                                {
-                                    contexts.pop();
-                                    contexts.push((
-                                        // We'll throw this away later
-                                        new_span, left_conts,
-                                    ));
-                                } else {
-                                    contexts.push((right, right_conts));
-                                }
-                            } else {
-                                contexts.push((right, right_conts));
-                            }
-                        }
-                    }
-                    for (ctx, _) in contexts {
-                        self.render_context(f, source, &ctx, &labels[..])?;
-                    }
+        let source = match opt_source {
+            Some(source) => source,
+            None => return Ok(()),
+        };
+        let labels = match diagnostic.labels() {
+            Some(labels) => labels,
+            None => return Ok(()),
+        };
+
+        let mut labels = labels.collect::<Vec<_>>();
+        labels.sort_unstable_by_key(|l| l.inner().offset());
+
+        let mut contexts = Vec::with_capacity(labels.len());
+        for right in labels.iter().cloned() {
+            let right_conts = source
+                .read_span(right.inner(), self.context_lines, self.context_lines)
+                .map_err(|_| fmt::Error)?;
+
+            if contexts.is_empty() {
+                contexts.push((right, right_conts));
+                continue;
+            }
+
+            let (left, left_conts) = contexts.last().unwrap();
+            if left_conts.line() + left_conts.line_count() >= right_conts.line() {
+                // The snippets will overlap, so we create one Big Chunky Boi
+                let left_end = left.offset() + left.len();
+                let right_end = right.offset() + right.len();
+                let new_end = std::cmp::max(left_end, right_end);
+
+                let new_span = LabeledSpan::new(
+                    left.label().map(String::from),
+                    left.offset(),
+                    new_end - left.offset(),
+                );
+                // Check that the two contexts can be combined
+                if let Ok(new_conts) =
+                    source.read_span(new_span.inner(), self.context_lines, self.context_lines)
+                {
+                    contexts.pop();
+                    // We'll throw the contents away later
+                    contexts.push((new_span, new_conts));
+                    continue;
                 }
             }
+
+            contexts.push((right, right_conts));
         }
+        for (ctx, _) in contexts {
+            self.render_context(f, source, &ctx, &labels[..])?;
+        }
+
         Ok(())
     }
 
-    fn render_context<'a>(
+    fn render_context(
         &self,
         f: &mut impl fmt::Write,
-        source: &'a dyn SourceCode,
+        source: &dyn SourceCode,
         context: &LabeledSpan,
         labels: &[LabeledSpan],
     ) -> fmt::Result {
         let (contents, lines) = self.get_lines(source, context.inner())?;
 
+        // only consider labels from the context as primary label
+        let ctx_labels = labels.iter().filter(|l| {
+            context.inner().offset() <= l.inner().offset()
+                && l.inner().offset() + l.inner().len()
+                    <= context.inner().offset() + context.inner().len()
+        });
+        let primary_label = ctx_labels
+            .clone()
+            .find(|label| label.primary())
+            .or_else(|| ctx_labels.clone().next());
+
         // sorting is your friend
         let labels = labels
             .iter()
@@ -398,6 +493,8 @@
             .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st))
             .collect::<Vec<_>>();
 
+        let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents);
+
         // The max number of gutter-lines that will be active at any given
         // point. We need this to figure out indentation, so we do one loop
         // over the lines to see what the damage is gonna be.
@@ -405,7 +502,7 @@
         for line in &lines {
             let mut num_highlights = 0;
             for hl in &labels {
-                if !line.span_line_only(hl) && line.span_applies(hl) {
+                if !line.span_line_only(hl) && line.span_applies_gutter(hl) {
                     num_highlights += 1;
                 }
             }
@@ -431,19 +528,33 @@
             self.theme.characters.hbar,
         )?;
 
-        if let Some(source_name) = contents.name() {
+        // If there is a primary label, then use its span
+        // as the reference point for line/column information.
+        let primary_contents = match primary_label {
+            Some(label) => source
+                .read_span(label.inner(), 0, 0)
+                .map_err(|_| fmt::Error)?,
+            None => contents,
+        };
+
+        if let Some(source_name) = primary_contents.name() {
             let source_name = source_name.style(self.theme.styles.link);
             writeln!(
                 f,
                 "[{}:{}:{}]",
                 source_name,
-                contents.line() + 1,
-                contents.column() + 1
+                primary_contents.line() + 1,
+                primary_contents.column() + 1
             )?;
         } else if lines.len() <= 1 {
             writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
         } else {
-            writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?;
+            writeln!(
+                f,
+                "[{}:{}]",
+                primary_contents.line() + 1,
+                primary_contents.column() + 1
+            )?;
         }
 
         // Now it's time for the fun part--actually rendering everything!
@@ -457,7 +568,9 @@
             self.render_line_gutter(f, max_gutter, line, &labels)?;
 
             // And _now_ we can print out the line text itself!
-            self.render_line_text(f, &line.text)?;
+            let styled_text =
+                StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
+            self.render_line_text(f, &styled_text)?;
 
             // Next, we write all the highlights that apply to this particular line.
             let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
@@ -468,7 +581,13 @@
                 // no line number!
                 self.write_no_linum(f, linum_width)?;
                 // gutter _again_
-                self.render_highlight_gutter(f, max_gutter, line, &labels)?;
+                self.render_highlight_gutter(
+                    f,
+                    max_gutter,
+                    line,
+                    &labels,
+                    LabelRenderMode::SingleLine,
+                )?;
                 self.render_single_line_highlights(
                     f,
                     line,
@@ -480,11 +599,7 @@
             }
             for hl in multi_line {
                 if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
-                    // no line number!
-                    self.write_no_linum(f, linum_width)?;
-                    // gutter _again_
-                    self.render_highlight_gutter(f, max_gutter, line, &labels)?;
-                    self.render_multi_line_end(f, hl)?;
+                    self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
                 }
             }
         }
@@ -498,6 +613,91 @@
         Ok(())
     }
 
+    fn render_multi_line_end(
+        &self,
+        f: &mut impl fmt::Write,
+        labels: &[FancySpan],
+        max_gutter: usize,
+        linum_width: usize,
+        line: &Line,
+        label: &FancySpan,
+    ) -> fmt::Result {
+        // no line number!
+        self.write_no_linum(f, linum_width)?;
+
+        if let Some(label_parts) = label.label_parts() {
+            // if it has a label, how long is it?
+            let (first, rest) = label_parts
+                .split_first()
+                .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
+
+            if rest.is_empty() {
+                // gutter _again_
+                self.render_highlight_gutter(
+                    f,
+                    max_gutter,
+                    line,
+                    &labels,
+                    LabelRenderMode::SingleLine,
+                )?;
+
+                self.render_multi_line_end_single(
+                    f,
+                    first,
+                    label.style,
+                    LabelRenderMode::SingleLine,
+                )?;
+            } else {
+                // gutter _again_
+                self.render_highlight_gutter(
+                    f,
+                    max_gutter,
+                    line,
+                    &labels,
+                    LabelRenderMode::MultiLineFirst,
+                )?;
+
+                self.render_multi_line_end_single(
+                    f,
+                    first,
+                    label.style,
+                    LabelRenderMode::MultiLineFirst,
+                )?;
+                for label_line in rest {
+                    // no line number!
+                    self.write_no_linum(f, linum_width)?;
+                    // gutter _again_
+                    self.render_highlight_gutter(
+                        f,
+                        max_gutter,
+                        line,
+                        &labels,
+                        LabelRenderMode::MultiLineRest,
+                    )?;
+                    self.render_multi_line_end_single(
+                        f,
+                        label_line,
+                        label.style,
+                        LabelRenderMode::MultiLineRest,
+                    )?;
+                }
+            }
+        } else {
+            // gutter _again_
+            self.render_highlight_gutter(
+                f,
+                max_gutter,
+                line,
+                &labels,
+                LabelRenderMode::SingleLine,
+            )?;
+            // has no label
+            writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
+        }
+
+        Ok(())
+    }
+
     fn render_line_gutter(
         &self,
         f: &mut impl fmt::Write,
@@ -510,7 +710,7 @@
         }
         let chars = &self.theme.characters;
         let mut gutter = String::new();
-        let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
+        let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
         let mut arrow = false;
         for (i, hl) in applicable.enumerate() {
             if line.span_starts(hl) {
@@ -566,33 +766,121 @@
         max_gutter: usize,
         line: &Line,
         highlights: &[FancySpan],
+        render_mode: LabelRenderMode,
     ) -> fmt::Result {
         if max_gutter == 0 {
             return Ok(());
         }
+
+        // keeps track of how many colums wide the gutter is
+        // important for ansi since simply measuring the size of the final string
+        // gives the wrong result when the string contains ansi codes.
+        let mut gutter_cols = 0;
+
         let chars = &self.theme.characters;
         let mut gutter = String::new();
-        let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
+        let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
         for (i, hl) in applicable.enumerate() {
             if !line.span_line_only(hl) && line.span_ends(hl) {
-                gutter.push_str(&chars.lbot.style(hl.style).to_string());
-                gutter.push_str(
-                    &chars
-                        .hbar
-                        .to_string()
-                        .repeat(max_gutter.saturating_sub(i) + 2)
-                        .style(hl.style)
-                        .to_string(),
-                );
+                if render_mode == LabelRenderMode::MultiLineRest {
+                    // this is to make multiline labels work. We want to make the right amount
+                    // of horizontal space for them, but not actually draw the lines
+                    let horizontal_space = max_gutter.saturating_sub(i) + 2;
+                    for _ in 0..horizontal_space {
+                        gutter.push(' ');
+                    }
+                    // account for one more horizontal space, since in multiline mode
+                    // we also add in the vertical line before the label like this:
+                    // 2 │ ╭─▶   text
+                    // 3 │ ├─▶     here
+                    //   · ╰──┤ these two lines
+                    //   ·    │ are the problem
+                    //        ^this
+                    gutter_cols += horizontal_space + 1;
+                } else {
+                    let num_repeat = max_gutter.saturating_sub(i) + 2;
+
+                    gutter.push_str(&chars.lbot.style(hl.style).to_string());
+
+                    gutter.push_str(
+                        &chars
+                            .hbar
+                            .to_string()
+                            .repeat(
+                                num_repeat
+                                    // if we are rendering a multiline label, then leave a bit of space for the
+                                    // rcross character
+                                    - if render_mode == LabelRenderMode::MultiLineFirst {
+                                        1
+                                    } else {
+                                        0
+                                    },
+                            )
+                            .style(hl.style)
+                            .to_string(),
+                    );
+
+                    // we count 1 for the lbot char, and then a few more, the same number
+                    // as we just repeated for. For each repeat we only add 1, even though
+                    // due to ansi escape codes the number of bytes in the string could grow
+                    // a lot each time.
+                    gutter_cols += num_repeat + 1;
+                }
                 break;
             } else {
                 gutter.push_str(&chars.vbar.style(hl.style).to_string());
+
+                // we may push many bytes for the ansi escape codes style adds,
+                // but we still only add a single character-width to the string in a terminal
+                gutter_cols += 1;
             }
         }
-        write!(f, "{:width$}", gutter, width = max_gutter + 1)?;
+
+        // now calculate how many spaces to add based on how many columns we just created.
+        // it's the max width of the gutter, minus how many character-widths we just generated
+        // capped at 0 (though this should never go below in reality), and then we add 3 to
+        // account for arrowheads when a gutter line ends
+        let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols);
+        // we then write the gutter and as many spaces as we need
+        write!(f, "{}{:width$}", gutter, "", width = num_spaces)?;
         Ok(())
     }
 
+    fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
+        if self.wrap_lines {
+            textwrap::fill(text, opts)
+        } else {
+            // Format without wrapping, but retain the indentation options
+            // Implementation based on `textwrap::indent`
+            let mut result = String::with_capacity(2 * text.len());
+            let trimmed_indent = opts.subsequent_indent.trim_end();
+            for (idx, line) in text.split_terminator('\n').enumerate() {
+                if idx > 0 {
+                    result.push('\n');
+                }
+                if idx == 0 {
+                    if line.trim().is_empty() {
+                        result.push_str(opts.initial_indent.trim_end());
+                    } else {
+                        result.push_str(opts.initial_indent);
+                    }
+                } else {
+                    if line.trim().is_empty() {
+                        result.push_str(trimmed_indent);
+                    } else {
+                        result.push_str(opts.subsequent_indent);
+                    }
+                }
+                result.push_str(line);
+            }
+            if text.ends_with('\n') {
+                // split_terminator will have eaten the final '\n'.
+                result.push('\n');
+            }
+            result
+        }
+    }
+
     fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
         write!(
             f,
@@ -618,13 +906,26 @@
     /// Returns an iterator over the visual width of each character in a line.
     fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a {
         let mut column = 0;
+        let mut escaped = false;
         let tab_width = self.tab_width;
         text.chars().map(move |c| {
-            let width = if c == '\t' {
+            let width = match (escaped, c) {
                 // Round up to the next multiple of tab_width
-                tab_width - column % tab_width
-            } else {
-                c.width().unwrap_or(0)
+                (false, '\t') => tab_width - column % tab_width,
+                // start of ANSI escape
+                (false, '\x1b') => {
+                    escaped = true;
+                    0
+                }
+                // use Unicode width for all other characters
+                (false, c) => c.width().unwrap_or(0),
+                // end of ANSI escape
+                (true, 'm') => {
+                    escaped = false;
+                    0
+                }
+                // characters are zero width within escape sequence
+                (true, _) => 0,
             };
             column += width;
             width
@@ -632,11 +933,22 @@
     }
 
     /// Returns the visual column position of a byte offset on a specific line.
-    fn visual_offset(&self, line: &Line, offset: usize) -> usize {
+    ///
+    /// If the offset occurs in the middle of a character, the returned column
+    /// corresponds to that character's first column in `start` is true, or its
+    /// last column if `start` is false.
+    fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
         let line_range = line.offset..=(line.offset + line.length);
         assert!(line_range.contains(&offset));
 
-        let text_index = offset - line.offset;
+        let mut text_index = offset - line.offset;
+        while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
+            if start {
+                text_index -= 1;
+            } else {
+                text_index += 1;
+            }
+        }
         let text = &line.text[..text_index.min(line.text.len())];
         let text_width = self.line_visual_char_width(text).sum();
         if text_index > line.text.len() {
@@ -659,10 +971,10 @@
         for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
             if c == '\t' {
                 for _ in 0..width {
-                    f.write_char(' ')?
+                    f.write_char(' ')?;
                 }
             } else {
-                f.write_char(c)?
+                f.write_char(c)?;
             }
         }
         f.write_char('\n')?;
@@ -687,32 +999,34 @@
             .map(|hl| {
                 let byte_start = hl.offset();
                 let byte_end = hl.offset() + hl.len();
-                let start = self.visual_offset(line, byte_start).max(highest);
-                let end = self.visual_offset(line, byte_end).max(start + 1);
+                let start = self.visual_offset(line, byte_start, true).max(highest);
+                let end = if hl.len() == 0 {
+                    start + 1
+                } else {
+                    self.visual_offset(line, byte_end, false).max(start + 1)
+                };
 
                 let vbar_offset = (start + end) / 2;
                 let num_left = vbar_offset - start;
                 let num_right = end - vbar_offset - 1;
-                if start < end {
-                    underlines.push_str(
-                        &format!(
-                            "{:width$}{}{}{}",
-                            "",
-                            chars.underline.to_string().repeat(num_left),
-                            if hl.len() == 0 {
-                                chars.uarrow
-                            } else if hl.label().is_some() {
-                                chars.underbar
-                            } else {
-                                chars.underline
-                            },
-                            chars.underline.to_string().repeat(num_right),
-                            width = start.saturating_sub(highest),
-                        )
-                        .style(hl.style)
-                        .to_string(),
-                    );
-                }
+                underlines.push_str(
+                    &format!(
+                        "{:width$}{}{}{}",
+                        "",
+                        chars.underline.to_string().repeat(num_left),
+                        if hl.len() == 0 {
+                            chars.uarrow
+                        } else if hl.label().is_some() {
+                            chars.underbar
+                        } else {
+                            chars.underline
+                        },
+                        chars.underline.to_string().repeat(num_right),
+                        width = start.saturating_sub(highest),
+                    )
+                    .style(hl.style)
+                    .to_string(),
+                );
                 highest = std::cmp::max(highest, end);
 
                 (hl, vbar_offset)
@@ -721,27 +1035,40 @@
         writeln!(f, "{}", underlines)?;
 
         for hl in single_liners.iter().rev() {
-            if let Some(label) = hl.label() {
-                self.write_no_linum(f, linum_width)?;
-                self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
-                let mut curr_offset = 1usize;
-                for (offset_hl, vbar_offset) in &vbar_offsets {
-                    while curr_offset < *vbar_offset + 1 {
-                        write!(f, " ")?;
-                        curr_offset += 1;
-                    }
-                    if *offset_hl != hl {
-                        write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
-                        curr_offset += 1;
-                    } else {
-                        let lines = format!(
-                            "{}{} {}",
-                            chars.lbot,
-                            chars.hbar.to_string().repeat(2),
-                            label,
-                        );
-                        writeln!(f, "{}", lines.style(hl.style))?;
-                        break;
+            if let Some(label) = hl.label_parts() {
+                if label.len() == 1 {
+                    self.write_label_text(
+                        f,
+                        line,
+                        linum_width,
+                        max_gutter,
+                        all_highlights,
+                        chars,
+                        &vbar_offsets,
+                        hl,
+                        &label[0],
+                        LabelRenderMode::SingleLine,
+                    )?;
+                } else {
+                    let mut first = true;
+                    for label_line in &label {
+                        self.write_label_text(
+                            f,
+                            line,
+                            linum_width,
+                            max_gutter,
+                            all_highlights,
+                            chars,
+                            &vbar_offsets,
+                            hl,
+                            label_line,
+                            if first {
+                                LabelRenderMode::MultiLineFirst
+                            } else {
+                                LabelRenderMode::MultiLineRest
+                            },
+                        )?;
+                        first = false;
                     }
                 }
             }
@@ -749,13 +1076,80 @@
         Ok(())
     }
 
-    fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
-        writeln!(
+    // I know it's not good practice, but making this a function makes a lot of sense
+    // and making a struct for this does not...
+    #[allow(clippy::too_many_arguments)]
+    fn write_label_text(
+        &self,
+        f: &mut impl fmt::Write,
+        line: &Line,
+        linum_width: usize,
+        max_gutter: usize,
+        all_highlights: &[FancySpan],
+        chars: &ThemeCharacters,
+        vbar_offsets: &[(&&FancySpan, usize)],
+        hl: &&FancySpan,
+        label: &str,
+        render_mode: LabelRenderMode,
+    ) -> fmt::Result {
+        self.write_no_linum(f, linum_width)?;
+        self.render_highlight_gutter(
             f,
-            "{} {}",
-            self.theme.characters.hbar.style(hl.style),
-            hl.label().unwrap_or_else(|| "".into()),
+            max_gutter,
+            line,
+            all_highlights,
+            LabelRenderMode::SingleLine,
         )?;
+        let mut curr_offset = 1usize;
+        for (offset_hl, vbar_offset) in vbar_offsets {
+            while curr_offset < *vbar_offset + 1 {
+                write!(f, " ")?;
+                curr_offset += 1;
+            }
+            if *offset_hl != hl {
+                write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
+                curr_offset += 1;
+            } else {
+                let lines = match render_mode {
+                    LabelRenderMode::SingleLine => format!(
+                        "{}{} {}",
+                        chars.lbot,
+                        chars.hbar.to_string().repeat(2),
+                        label,
+                    ),
+                    LabelRenderMode::MultiLineFirst => {
+                        format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
+                    }
+                    LabelRenderMode::MultiLineRest => {
+                        format!("  {} {}", chars.vbar, label,)
+                    }
+                };
+                writeln!(f, "{}", lines.style(hl.style))?;
+                break;
+            }
+        }
+        Ok(())
+    }
+
+    fn render_multi_line_end_single(
+        &self,
+        f: &mut impl fmt::Write,
+        label: &str,
+        style: Style,
+        render_mode: LabelRenderMode,
+    ) -> fmt::Result {
+        match render_mode {
+            LabelRenderMode::SingleLine => {
+                writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
+            }
+            LabelRenderMode::MultiLineFirst => {
+                writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
+            }
+            LabelRenderMode::MultiLineRest => {
+                writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
+            }
+        }
+
         Ok(())
     }
 
@@ -834,6 +1228,16 @@
 Support types
 */
 
+#[derive(PartialEq, Debug)]
+enum LabelRenderMode {
+    /// we're rendering a single line label (or not rendering in any special way)
+    SingleLine,
+    /// we're rendering a multiline label
+    MultiLineFirst,
+    /// we're rendering the rest of a multiline label
+    MultiLineRest,
+}
+
 #[derive(Debug)]
 struct Line {
     line_number: usize,
@@ -847,14 +1251,31 @@
         span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
     }
 
+    /// Returns whether `span` should be visible on this line, either in the gutter or under the
+    /// text on this line
     fn span_applies(&self, span: &FancySpan) -> bool {
         let spanlen = if span.len() == 0 { 1 } else { span.len() };
         // Span starts in this line
+
         (span.offset() >= self.offset && span.offset() < self.offset + self.length)
-        // Span passes through this line
-        || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
-        // Span ends on this line
-        || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
+            // Span passes through this line
+            || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
+            // Span ends on this line
+            || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
+    }
+
+    /// Returns whether `span` should be visible on this line in the gutter (so this excludes spans
+    /// that are only visible on this line and do not span multiple lines)
+    fn span_applies_gutter(&self, span: &FancySpan) -> bool {
+        let spanlen = if span.len() == 0 { 1 } else { span.len() };
+        // Span starts in this line
+        self.span_applies(span)
+            && !(
+                // as long as it doesn't start *and* end on this line
+                (span.offset() >= self.offset && span.offset() < self.offset + self.length)
+                    && (span.offset() + spanlen > self.offset
+                        && span.offset() + spanlen <= self.offset + self.length)
+            )
     }
 
     // A 'flyby' is a multi-line span that technically covers this line, but
@@ -884,7 +1305,10 @@
 
 #[derive(Debug, Clone)]
 struct FancySpan {
-    label: Option<String>,
+    /// this is deliberately an option of a vec because I wanted to be very explicit
+    /// that there can also be *no* label. If there is a label, it can have multiple
+    /// lines which is what the vec is for.
+    label: Option<Vec<String>>,
     span: SourceSpan,
     style: Style,
 }
@@ -895,9 +1319,17 @@
     }
 }
 
+fn split_label(v: String) -> Vec<String> {
+    v.split('\n').map(|i| i.to_string()).collect()
+}
+
 impl FancySpan {
     fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
-        FancySpan { label, span, style }
+        FancySpan {
+            label: label.map(split_label),
+            span,
+            style,
+        }
     }
 
     fn style(&self) -> Style {
@@ -907,7 +1339,15 @@
     fn label(&self) -> Option<String> {
         self.label
             .as_ref()
-            .map(|l| l.style(self.style()).to_string())
+            .map(|l| l.join("\n").style(self.style()).to_string())
+    }
+
+    fn label_parts(&self) -> Option<Vec<String>> {
+        self.label.as_ref().map(|l| {
+            l.iter()
+                .map(|i| i.style(self.style()).to_string())
+                .collect()
+        })
     }
 
     fn offset(&self) -> usize {
diff --git a/crates/miette/src/handlers/json.rs b/crates/miette/src/handlers/json.rs
index 29e21a0..0b4a405 100644
--- a/crates/miette/src/handlers/json.rs
+++ b/crates/miette/src/handlers/json.rs
@@ -96,7 +96,7 @@
                 }
                 write!(f, r#""{}""#, escape(&error.to_string()))?;
             }
-            write!(f, "],")?
+            write!(f, "],")?;
         } else {
             write!(f, r#""causes": [],"#)?;
         }
diff --git a/crates/miette/src/handlers/theme.rs b/crates/miette/src/handlers/theme.rs
index 1f5236a..892ffc2 100644
--- a/crates/miette/src/handlers/theme.rs
+++ b/crates/miette/src/handlers/theme.rs
@@ -1,4 +1,5 @@
-use is_terminal::IsTerminal;
+use std::io::IsTerminal;
+
 use owo_colors::Style;
 
 /**
@@ -55,9 +56,9 @@
 
     /// A "basic" graphical theme that skips colors and unicode characters and
     /// just does monochrome ascii art. If you want a completely non-graphical
-    /// rendering of your `Diagnostic`s, check out
-    /// [crate::NarratableReportHandler], or write your own
-    /// [crate::ReportHandler]!
+    /// rendering of your [`Diagnostic`](crate::Diagnostic)s, check out
+    /// [`NarratableReportHandler`](crate::NarratableReportHandler), or write
+    /// your own [`ReportHandler`](crate::ReportHandler)
     pub fn none() -> Self {
         Self {
             characters: ThemeCharacters::ascii(),
@@ -79,7 +80,8 @@
 }
 
 /**
-Styles for various parts of graphical rendering for the [crate::GraphicalReportHandler].
+Styles for various parts of graphical rendering for the
+[`GraphicalReportHandler`](crate::GraphicalReportHandler).
 */
 #[derive(Debug, Clone)]
 pub struct ThemeStyles {
@@ -159,7 +161,7 @@
 // https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
 
 /// Characters to be used when drawing when using
-/// [crate::GraphicalReportHandler].
+/// [`GraphicalReportHandler`](crate::GraphicalReportHandler).
 #[allow(missing_docs)]
 #[derive(Debug, Clone, Eq, PartialEq)]
 pub struct ThemeCharacters {
diff --git a/crates/miette/src/highlighters/blank.rs b/crates/miette/src/highlighters/blank.rs
new file mode 100644
index 0000000..50a9c65
--- /dev/null
+++ b/crates/miette/src/highlighters/blank.rs
@@ -0,0 +1,36 @@
+use owo_colors::Style;
+
+use crate::SpanContents;
+
+use super::{Highlighter, HighlighterState};
+
+/// The default syntax highlighter. It applies `Style::default()` to input text.
+/// This is used by default when no syntax highlighting features are enabled.
+#[derive(Debug, Clone)]
+pub struct BlankHighlighter;
+
+impl Highlighter for BlankHighlighter {
+    fn start_highlighter_state<'h>(
+        &'h self,
+        _source: &dyn SpanContents<'_>,
+    ) -> Box<dyn super::HighlighterState + 'h> {
+        Box::new(BlankHighlighterState)
+    }
+}
+
+impl Default for BlankHighlighter {
+    fn default() -> Self {
+        BlankHighlighter
+    }
+}
+
+/// The default highlighter state. It applies `Style::default()` to input text.
+/// This is used by default when no syntax highlighting features are enabled.
+#[derive(Debug, Clone)]
+pub struct BlankHighlighterState;
+
+impl HighlighterState for BlankHighlighterState {
+    fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<owo_colors::Styled<&'s str>> {
+        vec![Style::default().style(line)]
+    }
+}
diff --git a/crates/miette/src/highlighters/mod.rs b/crates/miette/src/highlighters/mod.rs
new file mode 100644
index 0000000..d605c1c
--- /dev/null
+++ b/crates/miette/src/highlighters/mod.rs
@@ -0,0 +1,116 @@
+//! This module provides a trait for creating custom syntax highlighters that
+//! highlight [`Diagnostic`](crate::Diagnostic) source code with ANSI escape
+//! sequences when rendering with the [`GraphicalReportHighlighter`](crate::GraphicalReportHandler).
+//!
+//! It also provides built-in highlighter implementations that you can use out of the box.
+//! By default, there are no syntax highlighters exported by miette
+//! (except for the no-op [`BlankHighlighter`]).
+//! To enable support for specific highlighters, you should enable their associated feature flag.
+//!
+//! Currently supported syntax highlighters and their feature flags:
+//! * `syntect-highlighter` - Enables [`syntect`](https://docs.rs/syntect/latest/syntect/) syntax highlighting support via the [`SyntectHighlighter`]
+//!
+
+use std::{ops::Deref, sync::Arc};
+
+use crate::SpanContents;
+use owo_colors::Styled;
+
+#[cfg(feature = "syntect-highlighter")]
+pub use self::syntect::*;
+pub use blank::*;
+
+mod blank;
+#[cfg(feature = "syntect-highlighter")]
+mod syntect;
+
+/// A syntax highlighter for highlighting miette [`SourceCode`](crate::SourceCode) snippets.
+pub trait Highlighter {
+    ///  Creates a new [HighlighterState] to begin parsing and highlighting
+    /// a [SpanContents].
+    ///
+    /// The [GraphicalReportHandler](crate::GraphicalReportHandler) will call
+    /// this method at the start of rendering a [SpanContents].
+    ///
+    /// The [SpanContents] is provided as input only so that the [Highlighter]
+    /// can detect language syntax and make other initialization decisions prior
+    /// to highlighting, but it is not intended that the Highlighter begin
+    /// highlighting at this point. The returned [HighlighterState] is
+    /// responsible for the actual rendering.
+    fn start_highlighter_state<'h>(
+        &'h self,
+        source: &dyn SpanContents<'_>,
+    ) -> Box<dyn HighlighterState + 'h>;
+}
+
+/// A stateful highlighter that incrementally highlights lines of a particular
+/// source code.
+///
+/// The [GraphicalReportHandler](crate::GraphicalReportHandler)
+/// will create a highlighter state by calling
+/// [start_highlighter_state](Highlighter::start_highlighter_state) at the
+/// start of rendering, then it will iteratively call
+/// [highlight_line](HighlighterState::highlight_line) to render individual
+/// highlighted lines. This allows [Highlighter] implementations to maintain
+/// mutable parsing and highlighting state.
+pub trait HighlighterState {
+    /// Highlight an individual line from the source code by returning a vector of [Styled]
+    /// regions.
+    fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>>;
+}
+
+/// Arcified trait object for Highlighter. Used internally by [GraphicalReportHandler]
+///
+/// Wrapping the trait object in this way allows us to implement Debug and Clone.
+#[derive(Clone)]
+#[repr(transparent)]
+pub(crate) struct MietteHighlighter(Arc<dyn Highlighter + Send + Sync>);
+
+impl MietteHighlighter {
+    pub(crate) fn nocolor() -> Self {
+        Self::from(BlankHighlighter)
+    }
+
+    #[cfg(feature = "syntect-highlighter")]
+    pub(crate) fn syntect_truecolor() -> Self {
+        Self::from(SyntectHighlighter::default())
+    }
+}
+
+impl Default for MietteHighlighter {
+    #[cfg(feature = "syntect-highlighter")]
+    fn default() -> Self {
+        use std::io::IsTerminal;
+        match std::env::var("NO_COLOR") {
+            _ if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() => {
+                //TODO: should use ANSI styling instead of 24-bit truecolor here
+                Self(Arc::new(SyntectHighlighter::default()))
+            }
+            Ok(string) if string != "0" => MietteHighlighter::nocolor(),
+            _ => Self(Arc::new(SyntectHighlighter::default())),
+        }
+    }
+    #[cfg(not(feature = "syntect-highlighter"))]
+    fn default() -> Self {
+        return MietteHighlighter::nocolor();
+    }
+}
+
+impl<T: Highlighter + Send + Sync + 'static> From<T> for MietteHighlighter {
+    fn from(value: T) -> Self {
+        Self(Arc::new(value))
+    }
+}
+
+impl std::fmt::Debug for MietteHighlighter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "MietteHighlighter(...)")
+    }
+}
+
+impl Deref for MietteHighlighter {
+    type Target = dyn Highlighter + Send + Sync;
+    fn deref(&self) -> &Self::Target {
+        &*self.0
+    }
+}
diff --git a/crates/miette/src/highlighters/syntect.rs b/crates/miette/src/highlighters/syntect.rs
new file mode 100644
index 0000000..57ebadf
--- /dev/null
+++ b/crates/miette/src/highlighters/syntect.rs
@@ -0,0 +1,170 @@
+use std::path::Path;
+
+// all syntect imports are explicitly qualified, but their paths are shortened for convenience
+mod syntect {
+    pub(super) use syntect::{
+        highlighting::{
+            Color, HighlightIterator, HighlightState, Highlighter, Style, Theme, ThemeSet,
+        },
+        parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
+    };
+}
+
+use owo_colors::{Rgb, Style, Styled};
+
+use crate::{
+    highlighters::{Highlighter, HighlighterState},
+    SpanContents,
+};
+
+use super::BlankHighlighterState;
+
+/// Highlights miette [SourceCode] with the [syntect](https://docs.rs/syntect/latest/syntect/) highlighting crate.
+///
+/// Currently only 24-bit truecolor output is supported due to syntect themes
+/// representing color as RGBA.
+#[derive(Debug, Clone)]
+pub struct SyntectHighlighter {
+    theme: syntect::Theme,
+    syntax_set: syntect::SyntaxSet,
+    use_bg_color: bool,
+}
+
+impl Default for SyntectHighlighter {
+    fn default() -> Self {
+        let theme_set = syntect::ThemeSet::load_defaults();
+        let theme = theme_set.themes["base16-ocean.dark"].clone();
+        Self::new_themed(theme, false)
+    }
+}
+
+impl Highlighter for SyntectHighlighter {
+    fn start_highlighter_state<'h>(
+        &'h self,
+        source: &dyn SpanContents<'_>,
+    ) -> Box<dyn HighlighterState + 'h> {
+        if let Some(syntax) = self.detect_syntax(source) {
+            let highlighter = syntect::Highlighter::new(&self.theme);
+            let parse_state = syntect::ParseState::new(syntax);
+            let highlight_state =
+                syntect::HighlightState::new(&highlighter, syntect::ScopeStack::new());
+            Box::new(SyntectHighlighterState {
+                syntax_set: &self.syntax_set,
+                highlighter,
+                parse_state,
+                highlight_state,
+                use_bg_color: self.use_bg_color,
+            })
+        } else {
+            Box::new(BlankHighlighterState)
+        }
+    }
+}
+
+impl SyntectHighlighter {
+    /// Create a syntect highlighter with the given theme and syntax set.
+    pub fn new(syntax_set: syntect::SyntaxSet, theme: syntect::Theme, use_bg_color: bool) -> Self {
+        Self {
+            theme,
+            syntax_set,
+            use_bg_color,
+        }
+    }
+
+    /// Create a syntect highlighter with the given theme and the default syntax set.
+    pub fn new_themed(theme: syntect::Theme, use_bg_color: bool) -> Self {
+        Self::new(
+            syntect::SyntaxSet::load_defaults_nonewlines(),
+            theme,
+            use_bg_color,
+        )
+    }
+
+    /// Determine syntect SyntaxReference to use for given SourceCode
+    fn detect_syntax(&self, contents: &dyn SpanContents<'_>) -> Option<&syntect::SyntaxReference> {
+        // use language if given
+        if let Some(language) = contents.language() {
+            return self.syntax_set.find_syntax_by_name(language);
+        }
+        // otherwise try to use any file extension provided in the name
+        if let Some(name) = contents.name() {
+            if let Some(ext) = Path::new(name).extension() {
+                return self
+                    .syntax_set
+                    .find_syntax_by_extension(ext.to_string_lossy().as_ref());
+            }
+        }
+        // finally, attempt to guess syntax based on first line
+        return self.syntax_set.find_syntax_by_first_line(
+            &std::str::from_utf8(contents.data())
+                .ok()?
+                .split('\n')
+                .next()?,
+        );
+    }
+}
+
+/// Stateful highlighting iterator for [SyntectHighlighter]
+#[derive(Debug)]
+pub(crate) struct SyntectHighlighterState<'h> {
+    syntax_set: &'h syntect::SyntaxSet,
+    highlighter: syntect::Highlighter<'h>,
+    parse_state: syntect::ParseState,
+    highlight_state: syntect::HighlightState,
+    use_bg_color: bool,
+}
+
+impl<'h> HighlighterState for SyntectHighlighterState<'h> {
+    fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>> {
+        if let Ok(ops) = self.parse_state.parse_line(line, &self.syntax_set) {
+            let use_bg_color = self.use_bg_color;
+            syntect::HighlightIterator::new(
+                &mut self.highlight_state,
+                &ops,
+                line,
+                &mut self.highlighter,
+            )
+            .map(|(style, str)| (convert_style(style, use_bg_color).style(str)))
+            .collect()
+        } else {
+            vec![Style::default().style(line)]
+        }
+    }
+}
+
+/// Convert syntect [syntect::Style] into owo_colors [Style] */
+#[inline]
+fn convert_style(syntect_style: syntect::Style, use_bg_color: bool) -> Style {
+    if use_bg_color {
+        let fg = blend_fg_color(syntect_style);
+        let bg = convert_color(syntect_style.background);
+        Style::new().color(fg).on_color(bg)
+    } else {
+        let fg = convert_color(syntect_style.foreground);
+        Style::new().color(fg)
+    }
+}
+
+/// Blend foreground RGB into background RGB according to alpha channel
+#[inline]
+fn blend_fg_color(syntect_style: syntect::Style) -> Rgb {
+    let fg = syntect_style.foreground;
+    if fg.a == 0xff {
+        return convert_color(fg);
+    }
+    let bg = syntect_style.background;
+    let ratio = fg.a as u32;
+    let r = (fg.r as u32 * ratio + bg.r as u32 * (255 - ratio)) / 255;
+    let g = (fg.g as u32 * ratio + bg.g as u32 * (255 - ratio)) / 255;
+    let b = (fg.b as u32 * ratio + bg.b as u32 * (255 - ratio)) / 255;
+    Rgb(r as u8, g as u8, b as u8)
+}
+
+/// Convert syntect color into owo color.
+///
+/// Note: ignores alpha channel. use [`blend_fg_color`] if you need that
+///
+#[inline]
+fn convert_color(color: syntect::Color) -> Rgb {
+    Rgb(color.r, color.g, color.b)
+}
diff --git a/crates/miette/src/lib.rs b/crates/miette/src/lib.rs
index 20589ef..7674f2b 100644
--- a/crates/miette/src/lib.rs
+++ b/crates/miette/src/lib.rs
@@ -47,6 +47,7 @@
 //!   - [... delayed source code](#-delayed-source-code)
 //!   - [... handler options](#-handler-options)
 //!   - [... dynamic diagnostics](#-dynamic-diagnostics)
+//!   - [... syntax highlighting](#-syntax-highlighting)
 //! - [Acknowledgements](#acknowledgements)
 //! - [License](#license)
 //!
@@ -109,7 +110,7 @@
 //!     // The Source that we're gonna be printing snippets out of.
 //!     // This can be a String if you don't have or care about file names.
 //!     #[source_code]
-//!     src: NamedSource,
+//!     src: NamedSource<String>,
 //!     // Snippets and highlights can be included in the diagnostic!
 //!     #[label("This bit here")]
 //!     bad_bit: SourceSpan,
@@ -304,6 +305,23 @@
 //! miette = { version = "X.Y.Z", features = ["fancy"] }
 //! ```
 //!
+//! Another way to display a diagnostic is by printing them using the debug formatter.
+//! This is, in fact, what returning diagnostics from main ends up doing.
+//! To do it yourself, you can write the following:
+//!
+//! ```rust
+//! use miette::{IntoDiagnostic, Result};
+//! use semver::Version;
+//!
+//! fn just_a_random_function() {
+//!     let version_result: Result<Version> = "1.2.x".parse().into_diagnostic();
+//!     match version_result {
+//!         Err(e) => println!("{:?}", e),
+//!         Ok(version) => println!("{}", version),
+//!     }
+//! }
+//! ```
+//!
 //! ### ... diagnostic code URLs
 //!
 //! `miette` supports providing a URL for individual diagnostics. This URL will
@@ -593,6 +611,7 @@
 //!             .unicode(false)
 //!             .context_lines(3)
 //!             .tab_width(4)
+//!             .break_words(true)
 //!             .build(),
 //!     )
 //! }))
@@ -625,6 +644,38 @@
 //! println!("{:?}", report)
 //! ```
 //!
+//! ### ... syntax highlighting
+//!
+//! `miette` can be configured to highlight syntax in source code snippets.
+//!
+//! <!-- TODO: screenshot goes here once default Theme is decided -->
+//!
+//! To use the built-in highlighting functionality, you must enable the
+//! `syntect-highlighter` crate feature. When this feature is enabled, `miette` will
+//! automatically use the [`syntect`] crate to highlight the `#[source_code]`
+//! field of your [`Diagnostic`].
+//!
+//! Syntax detection with [`syntect`] is handled by checking 2 methods on the [`SpanContents`] trait, in order:
+//! * [language()](SpanContents::language) - Provides the name of the language
+//!   as a string. For example `"Rust"` will indicate Rust syntax highlighting.
+//!   You can set the language of the [`SpanContents`] produced by a
+//!   [`NamedSource`] via the [`with_language`](NamedSource::with_language)
+//!   method.
+//! * [name()](SpanContents::name) - In the absence of an explicitly set
+//!   language, the name is assumed to contain a file name or file path.
+//!   The highlighter will check for a file extension at the end of the name and
+//!   try to guess the syntax from that.
+//!
+//! If you want to use a custom highlighter, you can provide a custom
+//! implementation of the [`Highlighter`](highlighters::Highlighter)
+//! trait to [`MietteHandlerOpts`] by calling the
+//! [`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting)
+//! method. See the [`highlighters`] module docs for more details.
+//!
+//! ## MSRV
+//!
+//! This crate requires rustc 1.70.0 or later.
+//!
 //! ## Acknowledgements
 //!
 //! `miette` was not developed in a void. It owes enormous credit to various
@@ -652,6 +703,7 @@
 //! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also
 //! under the Apache License. Some code is taken from
 //! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
+#[cfg(feature = "derive")]
 pub use miette_derive::*;
 
 pub use error::*;
@@ -672,6 +724,8 @@
 #[cfg(feature = "fancy-no-backtrace")]
 mod handler;
 mod handlers;
+#[cfg(feature = "fancy-no-backtrace")]
+pub mod highlighters;
 #[doc(hidden)]
 pub mod macro_helpers;
 mod miette_diagnostic;
diff --git a/crates/miette/src/miette_diagnostic.rs b/crates/miette/src/miette_diagnostic.rs
index 67b75d0..9863e88 100644
--- a/crates/miette/src/miette_diagnostic.rs
+++ b/crates/miette/src/miette_diagnostic.rs
@@ -252,7 +252,7 @@
     /// ```
     pub fn and_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
         let mut all_labels = self.labels.unwrap_or_default();
-        all_labels.extend(labels.into_iter());
+        all_labels.extend(labels);
         self.labels = Some(all_labels);
         self
     }
@@ -292,14 +292,16 @@
                     "offset": 0,
                     "length": 0
                 },
-                "label": "label1"
+                "label": "label1",
+                "primary": false
             },
             {
                 "span": {
                     "offset": 1,
                     "length": 2
                 },
-                "label": "label2"
+                "label": "label2",
+                "primary": false
             }
         ]
     });
@@ -350,14 +352,16 @@
                     "offset": 0,
                     "length": 0
                 },
-                "label": "label1"
+                "label": "label1",
+                "primary": false
             },
             {
                 "span": {
                     "offset": 1,
                     "length": 2
                 },
-                "label": "label2"
+                "label": "label2",
+                "primary": false
             }
         ]
     });
diff --git a/crates/miette/src/named_source.rs b/crates/miette/src/named_source.rs
index 31ad1d1..99e74a8 100644
--- a/crates/miette/src/named_source.rs
+++ b/crates/miette/src/named_source.rs
@@ -3,27 +3,33 @@
 /// Utility struct for when you have a regular [`SourceCode`] type that doesn't
 /// implement `name`. For example [`String`]. Or if you want to override the
 /// `name` returned by the `SourceCode`.
-pub struct NamedSource {
-    source: Box<dyn SourceCode + 'static>,
+pub struct NamedSource<S: SourceCode + 'static> {
+    source: S,
     name: String,
+    language: Option<String>,
 }
 
-impl std::fmt::Debug for NamedSource {
+impl<S: SourceCode> std::fmt::Debug for NamedSource<S> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("NamedSource")
             .field("name", &self.name)
-            .field("source", &"<redacted>");
+            .field("source", &"<redacted>")
+            .field("language", &self.language);
         Ok(())
     }
 }
 
-impl NamedSource {
+impl<S: SourceCode + 'static> NamedSource<S> {
     /// Create a new `NamedSource` using a regular [`SourceCode`] and giving
     /// its returned [`SpanContents`] a name.
-    pub fn new(name: impl AsRef<str>, source: impl SourceCode + Send + Sync + 'static) -> Self {
+    pub fn new(name: impl AsRef<str>, source: S) -> Self
+    where
+        S: Send + Sync,
+    {
         Self {
-            source: Box::new(source),
+            source,
             name: name.as_ref().to_string(),
+            language: None,
         }
     }
 
@@ -34,28 +40,38 @@
 
     /// Returns a reference the inner [`SourceCode`] type for this
     /// `NamedSource`.
-    pub fn inner(&self) -> &(dyn SourceCode + 'static) {
-        &*self.source
+    pub fn inner(&self) -> &S {
+        &self.source
+    }
+
+    /// Sets the [`language`](SpanContents::language) for this source code.
+    pub fn with_language(mut self, language: impl Into<String>) -> Self {
+        self.language = Some(language.into());
+        self
     }
 }
 
-impl SourceCode for NamedSource {
+impl<S: SourceCode + 'static> SourceCode for NamedSource<S> {
     fn read_span<'a>(
         &'a self,
         span: &crate::SourceSpan,
         context_lines_before: usize,
         context_lines_after: usize,
     ) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError> {
-        let contents = self
-            .inner()
-            .read_span(span, context_lines_before, context_lines_after)?;
-        Ok(Box::new(MietteSpanContents::new_named(
+        let inner_contents =
+            self.inner()
+                .read_span(span, context_lines_before, context_lines_after)?;
+        let mut contents = MietteSpanContents::new_named(
             self.name.clone(),
-            contents.data(),
-            *contents.span(),
-            contents.line(),
-            contents.column(),
-            contents.line_count(),
-        )))
+            inner_contents.data(),
+            *inner_contents.span(),
+            inner_contents.line(),
+            inner_contents.column(),
+            inner_contents.line_count(),
+        );
+        if let Some(language) = &self.language {
+            contents = contents.with_language(language);
+        }
+        Ok(Box::new(contents))
     }
 }
diff --git a/crates/miette/src/protocol.rs b/crates/miette/src/protocol.rs
index f516984..6b98a97 100644
--- a/crates/miette/src/protocol.rs
+++ b/crates/miette/src/protocol.rs
@@ -179,6 +179,7 @@
 */
 #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Default)]
 pub enum Severity {
     /// Just some help. Here's how you could be doing it better.
     Advice,
@@ -186,15 +187,10 @@
     Warning,
     /// Critical failure. The program cannot continue.
     /// This is the default severity, if you don't specify another one.
+    #[default]
     Error,
 }
 
-impl Default for Severity {
-    fn default() -> Self {
-        Severity::Error
-    }
-}
-
 #[cfg(feature = "serde")]
 #[test]
 fn test_serialize_severity() {
@@ -232,7 +228,7 @@
 gigabytes or larger in size.
 */
 pub trait SourceCode: Send + Sync {
-    /// Read the bytes for a specific span from this SourceCode, keeping a
+    /// Read the bytes for a specific span from this `SourceCode`, keeping a
     /// certain number of lines before and after the span as context.
     fn read_span<'a>(
         &'a self,
@@ -249,6 +245,7 @@
     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
     label: Option<String>,
     span: SourceSpan,
+    primary: bool,
 }
 
 impl LabeledSpan {
@@ -256,7 +253,8 @@
     pub const fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
         Self {
             label,
-            span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)),
+            span: SourceSpan::new(SourceOffset(offset), len),
+            primary: false,
         }
     }
 
@@ -265,6 +263,16 @@
         Self {
             label,
             span: span.into(),
+            primary: false,
+        }
+    }
+
+    /// Makes a new labeled primary span using an existing span.
+    pub fn new_primary_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
+        Self {
+            label,
+            span: span.into(),
+            primary: true,
         }
     }
 
@@ -340,6 +348,11 @@
     pub const fn is_empty(&self) -> bool {
         self.span.is_empty()
     }
+
+    /// True if this `LabeledSpan` is a primary span.
+    pub const fn primary(&self) -> bool {
+        self.primary
+    }
 }
 
 #[cfg(feature = "serde")]
@@ -350,7 +363,8 @@
     assert_eq!(
         json!(LabeledSpan::new(None, 0, 0)),
         json!({
-            "span": { "offset": 0, "length": 0 }
+            "span": { "offset": 0, "length": 0, },
+            "primary": false,
         })
     );
 
@@ -358,9 +372,10 @@
         json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
         json!({
             "label": "label",
-            "span": { "offset": 0, "length": 0 }
+            "span": { "offset": 0, "length": 0, },
+            "primary": false,
         })
-    )
+    );
 }
 
 #[cfg(feature = "serde")]
@@ -370,23 +385,26 @@
 
     let span: LabeledSpan = serde_json::from_value(json!({
         "label": null,
-        "span": { "offset": 0, "length": 0 }
+        "span": { "offset": 0, "length": 0, },
+        "primary": false,
     }))
     .unwrap();
     assert_eq!(span, LabeledSpan::new(None, 0, 0));
 
     let span: LabeledSpan = serde_json::from_value(json!({
-        "span": { "offset": 0, "length": 0 }
+        "span": { "offset": 0, "length": 0, },
+        "primary": false
     }))
     .unwrap();
     assert_eq!(span, LabeledSpan::new(None, 0, 0));
 
     let span: LabeledSpan = serde_json::from_value(json!({
         "label": "label",
-        "span": { "offset": 0, "length": 0 }
+        "span": { "offset": 0, "length": 0, },
+        "primary": false
     }))
     .unwrap();
-    assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0))
+    assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0));
 }
 
 /**
@@ -411,6 +429,15 @@
     fn column(&self) -> usize;
     /// Total number of lines covered by this `SpanContents`.
     fn line_count(&self) -> usize;
+
+    /// Optional method. The language name for this source code, if any.
+    /// This is used to drive syntax highlighting.
+    ///
+    /// Examples: Rust, TOML, C
+    ///
+    fn language(&self) -> Option<&str> {
+        None
+    }
 }
 
 /**
@@ -430,6 +457,8 @@
     line_count: usize,
     // Optional filename
     name: Option<String>,
+    // Optional language
+    language: Option<String>,
 }
 
 impl<'a> MietteSpanContents<'a> {
@@ -448,6 +477,7 @@
             column,
             line_count,
             name: None,
+            language: None,
         }
     }
 
@@ -467,8 +497,15 @@
             column,
             line_count,
             name: Some(name),
+            language: None,
         }
     }
+
+    /// Sets the [`language`](SourceCode::language) for syntax highlighting.
+    pub fn with_language(mut self, language: impl Into<String>) -> Self {
+        self.language = Some(language.into());
+        self
+    }
 }
 
 impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
@@ -490,6 +527,9 @@
     fn name(&self) -> Option<&str> {
         self.name.as_deref()
     }
+    fn language(&self) -> Option<&str> {
+        self.language.as_deref()
+    }
 }
 
 /// Span within a [`SourceCode`]
@@ -504,10 +544,10 @@
 
 impl SourceSpan {
     /// Create a new [`SourceSpan`].
-    pub const fn new(start: SourceOffset, length: SourceOffset) -> Self {
+    pub const fn new(start: SourceOffset, length: usize) -> Self {
         Self {
             offset: start,
-            length: length.offset(),
+            length,
         }
     }
 
@@ -537,8 +577,8 @@
     }
 }
 
-impl From<(SourceOffset, SourceOffset)> for SourceSpan {
-    fn from((start, len): (SourceOffset, SourceOffset)) -> Self {
+impl From<(SourceOffset, usize)> for SourceSpan {
+    fn from((start, len): (SourceOffset, usize)) -> Self {
         Self::new(start, len)
     }
 }
@@ -575,7 +615,7 @@
     assert_eq!(
         json!(SourceSpan::from(0)),
         json!({ "offset": 0, "length": 0})
-    )
+    );
 }
 
 #[cfg(feature = "serde")]
@@ -584,7 +624,7 @@
     use serde_json::json;
 
     let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap();
-    assert_eq!(span, SourceSpan::from(0))
+    assert_eq!(span, SourceSpan::from(0));
 }
 
 /**
@@ -686,12 +726,12 @@
 fn test_serialize_source_offset() {
     use serde_json::json;
 
-    assert_eq!(json!(SourceOffset::from(0)), 0)
+    assert_eq!(json!(SourceOffset::from(0)), 0);
 }
 
 #[cfg(feature = "serde")]
 #[test]
 fn test_deserialize_source_offset() {
     let offset: SourceOffset = serde_json::from_str("0").unwrap();
-    assert_eq!(offset, SourceOffset::from(0))
+    assert_eq!(offset, SourceOffset::from(0));
 }
diff --git a/pseudo_crate/Cargo.lock b/pseudo_crate/Cargo.lock
index 6864911..ea1f5ea 100644
--- a/pseudo_crate/Cargo.lock
+++ b/pseudo_crate/Cargo.lock
@@ -3877,21 +3877,20 @@
 
 [[package]]
 name = "miette"
-version = "5.10.0"
+version = "6.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"
+checksum = "337e1043bbc086dac9d9674983bef52ac991ce150e09b5b8e35c5a73dd83f66c"
 dependencies = [
  "miette-derive",
- "once_cell",
  "thiserror 1.0.49",
  "unicode-width 0.1.11",
 ]
 
 [[package]]
 name = "miette-derive"
-version = "5.10.0"
+version = "6.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
+checksum = "71e622f2a0dd84cbca79bc6c3c33f4fd7dc69faf992216516aacc1d136102800"
 dependencies = [
  "proc-macro2 1.0.93",
  "quote 1.0.38",
diff --git a/pseudo_crate/Cargo.toml b/pseudo_crate/Cargo.toml
index 0faa577..d9551a0 100644
--- a/pseudo_crate/Cargo.toml
+++ b/pseudo_crate/Cargo.toml
@@ -214,8 +214,8 @@
 memoffset = "=0.9.1"
 merge = "=0.1.0"
 merge_derive = "=0.1.0"
-miette = "=5.10.0"
-miette-derive = "=5.10.0"
+miette = "=6.0.1"
+miette-derive = "=6.0.1"
 mime = "=0.3.17"
 minimal-lexical = "=0.2.1"
 mio = "=1.0.3"