Repository: toml-rs/toml (language: rust)

Issue:
Support reporting line/column in serialization errors
toml-rs [supported this](https://github.com/alexcrichton/toml-rs/blob/master/src/de.rs#L2166) even if [it didn't always work well](https://github.com/crate-ci/typos/issues/405)


Discussion / hints:
Some implementation is related to #302 

The main question is how to limit the cost.
The first step towards this is in #429 which adds span tracking

Fix patch:
diff --git a/crates/toml_edit/src/array.rs b/crates/toml_edit/src/array.rs
index 19b844ac..38fef03a 100644
--- a/crates/toml_edit/src/array.rs
+++ b/crates/toml_edit/src/array.rs
@@ -87,6 +87,12 @@ impl Array {
         &self.decor
     }
 
+    /// Returns the location within the original document
+    #[cfg(feature = "serde")]
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        self.span.clone()
+    }
+
     pub(crate) fn despan(&mut self, input: &str) {
         self.span = None;
         self.decor.despan(input);
diff --git a/crates/toml_edit/src/de/inline_table.rs b/crates/toml_edit/src/de/inline_table.rs
index d6dd3e19..becb3ea6 100644
--- a/crates/toml_edit/src/de/inline_table.rs
+++ b/crates/toml_edit/src/de/inline_table.rs
@@ -57,10 +57,12 @@ impl<'de> serde::Deserializer<'de> for crate::InlineTable {
         if self.is_empty() {
             Err(crate::de::Error::custom(
                 "wanted exactly 1 element, found 0 elements",
+                self.span(),
             ))
         } else if self.len() != 1 {
             Err(crate::de::Error::custom(
                 "wanted exactly 1 element, more than 1 element",
+                self.span(),
             ))
         } else {
             visitor.visit_enum(crate::de::InlineTableMapAccess::new(self))
@@ -84,13 +86,16 @@ impl<'de> serde::de::IntoDeserializer<'de, crate::de::Error> for crate::InlineTa
 
 pub(crate) struct InlineTableMapAccess {
     iter: indexmap::map::IntoIter<crate::InternalString, crate::table::TableKeyValue>,
+    span: Option<std::ops::Range<usize>>,
     value: Option<(crate::InternalString, crate::Item)>,
 }
 
 impl InlineTableMapAccess {
     pub(crate) fn new(input: crate::InlineTable) -> Self {
+        let span = input.span();
         Self {
             iter: input.items.into_iter(),
+            span,
             value: None,
         }
     }
@@ -105,7 +110,14 @@ impl<'de> serde::de::MapAccess<'de> for InlineTableMapAccess {
     {
         match self.iter.next() {
             Some((k, v)) => {
-                let ret = seed.deserialize(k.into_deserializer()).map(Some);
+                let ret = seed.deserialize(k.into_deserializer()).map(Some).map_err(
+                    |mut e: Self::Error| {
+                        if e.span().is_none() {
+                            e.set_span(v.key.span());
+                        }
+                        e
+                    },
+                );
                 self.value = Some((k, v.value));
                 ret
             }
@@ -118,12 +130,16 @@ impl<'de> serde::de::MapAccess<'de> for InlineTableMapAccess {
         V: serde::de::DeserializeSeed<'de>,
     {
         match self.value.take() {
-            Some((k, v)) => seed
-                .deserialize(crate::de::ItemDeserializer::new(v))
-                .map_err(|mut err| {
-                    err.parent_key(k);
-                    err
-                }),
+            Some((_k, v)) => {
+                let span = v.span();
+                seed.deserialize(crate::de::ItemDeserializer::new(v))
+                    .map_err(|mut err| {
+                        if err.span().is_none() {
+                            err.set_span(span);
+                        }
+                        err
+                    })
+            }
             None => {
                 panic!("no more values in next_value_seed, internal error in ValueDeserializer")
             }
@@ -144,11 +160,18 @@ impl<'de> serde::de::EnumAccess<'de> for InlineTableMapAccess {
             None => {
                 return Err(Error::custom(
                     "expected table with exactly 1 entry, found empty table",
+                    self.span,
                 ));
             }
         };
 
         seed.deserialize(key.into_deserializer())
+            .map_err(|mut e: Self::Error| {
+                if e.span().is_none() {
+                    e.set_span(value.key.span());
+                }
+                e
+            })
             .map(|val| (val, super::TableEnumDeserializer::new(value.value)))
     }
 }
diff --git a/crates/toml_edit/src/de/item.rs b/crates/toml_edit/src/de/item.rs
index 04dfccd8..238e59b1 100644
--- a/crates/toml_edit/src/de/item.rs
+++ b/crates/toml_edit/src/de/item.rs
@@ -46,7 +46,15 @@ impl<'de> serde::Deserializer<'de> for ItemDeserializer {
     where
         V: serde::de::Visitor<'de>,
     {
-        visitor.visit_newtype_struct(self)
+        let span = self.input.span();
+        visitor
+            .visit_newtype_struct(self)
+            .map_err(|mut e: Self::Error| {
+                if e.span().is_none() {
+                    e.set_span(span);
+                }
+                e
+            })
     }
 
     fn deserialize_struct<V>(
@@ -98,10 +106,28 @@ impl<'de> serde::Deserializer<'de> for crate::Item {
     where
         V: serde::de::Visitor<'de>,
     {
+        let span = self.span();
         match self {
-            crate::Item::None => visitor.visit_none(),
-            crate::Item::Value(v) => v.deserialize_any(visitor),
-            crate::Item::Table(v) => visitor.visit_map(crate::de::TableMapAccess::new(v)),
+            crate::Item::None => visitor.visit_none().map_err(|mut e: Self::Error| {
+                if e.span().is_none() {
+                    e.set_span(span);
+                }
+                e
+            }),
+            crate::Item::Value(v) => v.deserialize_any(visitor).map_err(|mut e: Self::Error| {
+                if e.span().is_none() {
+                    e.set_span(span);
+                }
+                e
+            }),
+            crate::Item::Table(v) => visitor
+                .visit_map(crate::de::TableMapAccess::new(v))
+                .map_err(|mut e: Self::Error| {
+                    if e.span().is_none() {
+                        e.set_span(span);
+                    }
+                    e
+                }),
             crate::Item::ArrayOfTables(v) => {
                 visitor.visit_seq(crate::de::ArraySeqAccess::with_array_of_tables(v))
             }
@@ -114,7 +140,13 @@ impl<'de> serde::Deserializer<'de> for crate::Item {
     where
         V: serde::de::Visitor<'de>,
     {
-        visitor.visit_some(self)
+        let span = self.span();
+        visitor.visit_some(self).map_err(|mut e: Self::Error| {
+            if e.span().is_none() {
+                e.set_span(span);
+            }
+            e
+        })
     }
 
     fn deserialize_newtype_struct<V>(
@@ -125,7 +157,15 @@ impl<'de> serde::Deserializer<'de> for crate::Item {
     where
         V: serde::de::Visitor<'de>,
     {
-        visitor.visit_newtype_struct(self)
+        let span = self.span();
+        visitor
+            .visit_newtype_struct(self)
+            .map_err(|mut e: Self::Error| {
+                if e.span().is_none() {
+                    e.set_span(span);
+                }
+                e
+            })
     }
 
     // Called when the type to deserialize is an enum, as opposed to a field in the type.
@@ -138,22 +178,40 @@ impl<'de> serde::Deserializer<'de> for crate::Item {
     where
         V: serde::de::Visitor<'de>,
     {
+        let span = self.span();
         match self {
-            crate::Item::Value(v) => v.deserialize_enum(name, variants, visitor),
+            crate::Item::Value(v) => {
+                v.deserialize_enum(name, variants, visitor)
+                    .map_err(|mut e: Self::Error| {
+                        if e.span().is_none() {
+                            e.set_span(span);
+                        }
+                        e
+                    })
+            }
             crate::Item::Table(v) => {
                 if v.is_empty() {
                     Err(crate::de::Error::custom(
                         "wanted exactly 1 element, found 0 elements",
+                        v.span(),
                     ))
                 } else if v.len() != 1 {
                     Err(crate::de::Error::custom(
                         "wanted exactly 1 element, more than 1 element",
+                        v.span(),
                     ))
                 } else {
-                    visitor.visit_enum(crate::de::TableMapAccess::new(v))
+                    visitor
+                        .visit_enum(crate::de::TableMapAccess::new(v))
+                        .map_err(|mut e: Self::Error| {
+                            if e.span().is_none() {
+                                e.set_span(span);
+                            }
+                            e
+                        })
                 }
             }
-            _ => Err(crate::de::Error::custom("wanted string or table")),
+            e => Err(crate::de::Error::custom("wanted string or table", e.span())),
         }
     }
 
diff --git a/crates/toml_edit/src/de/mod.rs b/crates/toml_edit/src/de/mod.rs
index 2343d1e8..e45d8c1b 100644
--- a/crates/toml_edit/src/de/mod.rs
+++ b/crates/toml_edit/src/de/mod.rs
@@ -21,39 +21,35 @@ use table_enum::*;
 /// Errors that can occur when deserializing a type.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Error {
-    inner: Box<ErrorInner>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-struct ErrorInner {
-    message: String,
-    reverse_key: Vec<crate::InternalString>,
-    line_col: Option<(usize, usize)>,
+    inner: crate::TomlError,
 }
 
 impl Error {
-    pub(crate) fn custom<T>(msg: T) -> Self
+    pub(crate) fn custom<T>(msg: T, span: Option<std::ops::Range<usize>>) -> Self
     where
         T: std::fmt::Display,
     {
         Error {
-            inner: Box::new(ErrorInner {
-                message: msg.to_string(),
-                reverse_key: Default::default(),
-                line_col: None,
-            }),
+            inner: crate::TomlError::custom(msg.to_string(), span),
         }
     }
 
-    pub(crate) fn parent_key(&mut self, key: crate::InternalString) {
-        self.inner.reverse_key.push(key);
+    /// The start/end index into the original document where the error occurred
+    pub fn span(&self) -> Option<std::ops::Range<usize>> {
+        self.inner.span()
+    }
+
+    pub(crate) fn set_span(&mut self, span: Option<std::ops::Range<usize>>) {
+        self.inner.set_span(span);
     }
 
     /// Produces a (line, column) pair of the position of the error if available
     ///
     /// All indexes are 0-based.
+    #[deprecated(since = "0.18.0", note = "See instead `Error::span`")]
     pub fn line_col(&self) -> Option<(usize, usize)> {
-        self.inner.line_col
+        #[allow(deprecated)]
+        self.inner.line_col()
     }
 }
 
@@ -62,41 +58,25 @@ impl serde::de::Error for Error {
     where
         T: std::fmt::Display,
     {
-        Error::custom(msg)
+        Error::custom(msg, None)
     }
 }
 
 impl std::fmt::Display for Error {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        self.inner.message.fmt(f)?;
-
-        if !self.inner.reverse_key.is_empty() {
-            write!(f, " for key `")?;
-            for (i, k) in self.inner.reverse_key.iter().rev().enumerate() {
-                if i > 0 {
-                    write!(f, ".")?;
-                }
-                write!(f, "{}", k)?;
-            }
-            write!(f, "`")?;
-        }
-
-        Ok(())
+        self.inner.fmt(f)
     }
 }
 
 impl From<crate::TomlError> for Error {
     fn from(e: crate::TomlError) -> Error {
-        let line_col = e.line_col();
-        let mut err = Self::custom(e);
-        err.inner.line_col = line_col;
-        err
+        Self { inner: e }
     }
 }
 
 impl From<Error> for crate::TomlError {
     fn from(e: Error) -> crate::TomlError {
-        Self::custom(e.to_string())
+        e.inner
     }
 }
 
@@ -116,7 +96,7 @@ pub fn from_slice<T>(s: &'_ [u8]) -> Result<T, Error>
 where
     T: DeserializeOwned,
 {
-    let s = std::str::from_utf8(s).map_err(Error::custom)?;
+    let s = std::str::from_utf8(s).map_err(|e| Error::custom(e, None))?;
     from_str(s)
 }
 
@@ -195,7 +175,13 @@ impl<'de> serde::Deserializer<'de> for crate::Document {
     where
         V: serde::de::Visitor<'de>,
     {
-        self.root.deserialize_any(visitor)
+        let original = self.original;
+        self.root
+            .deserialize_any(visitor)
+            .map_err(|mut e: Self::Error| {
+                e.inner.set_original(original);
+                e
+            })
     }
 
     // `None` is interpreted as a missing field so be sure to implement `Some`
@@ -204,7 +190,13 @@ impl<'de> serde::Deserializer<'de> for crate::Document {
     where
         V: serde::de::Visitor<'de>,
     {
-        self.root.deserialize_option(visitor)
+        let original = self.original;
+        self.root
+            .deserialize_option(visitor)
+            .map_err(|mut e: Self::Error| {
+                e.inner.set_original(original);
+                e
+            })
     }
 
     // Called when the type to deserialize is an enum, as opposed to a field in the type.
@@ -217,7 +209,13 @@ impl<'de> serde::Deserializer<'de> for crate::Document {
     where
         V: serde::de::Visitor<'de>,
     {
-        self.root.deserialize_enum(name, variants, visitor)
+        let original = self.original;
+        self.root
+            .deserialize_enum(name, variants, visitor)
+            .map_err(|mut e: Self::Error| {
+                e.inner.set_original(original);
+                e
+            })
     }
 
     serde::forward_to_deserialize_any! {
@@ -241,9 +239,9 @@ pub(crate) fn validate_struct_keys(
 ) -> Result<(), Error> {
     let extra_fields = table
         .iter()
-        .filter_map(|(key, _val)| {
+        .filter_map(|(key, val)| {
             if !fields.contains(&key.as_str()) {
-                Some(key.clone())
+                Some(val.clone())
             } else {
                 None
             }
@@ -253,10 +251,13 @@ pub(crate) fn validate_struct_keys(
     if extra_fields.is_empty() {
         Ok(())
     } else {
-        Err(Error::custom(format!(
-            "unexpected keys in table: {}, available keys: {}",
-            extra_fields.iter().join(", "),
-            fields.iter().join(", "),
-        )))
+        Err(Error::custom(
+            format!(
+                "unexpected keys in table: {}, available keys: {}",
+                extra_fields.iter().map(|k| k.key.get()).join(", "),
+                fields.iter().join(", "),
+            ),
+            extra_fields[0].key.span(),
+        ))
     }
 }
diff --git a/crates/toml_edit/src/de/table.rs b/crates/toml_edit/src/de/table.rs
index 8505c85b..fd5aca32 100644
--- a/crates/toml_edit/src/de/table.rs
+++ b/crates/toml_edit/src/de/table.rs
@@ -57,10 +57,12 @@ impl<'de> serde::Deserializer<'de> for crate::Table {
         if self.is_empty() {
             Err(crate::de::Error::custom(
                 "wanted exactly 1 element, found 0 elements",
+                self.span(),
             ))
         } else if self.len() != 1 {
             Err(crate::de::Error::custom(
                 "wanted exactly 1 element, more than 1 element",
+                self.span(),
             ))
         } else {
             visitor.visit_enum(crate::de::TableMapAccess::new(self))
@@ -84,13 +86,16 @@ impl<'de> serde::de::IntoDeserializer<'de, crate::de::Error> for crate::Table {
 
 pub(crate) struct TableMapAccess {
     iter: indexmap::map::IntoIter<crate::InternalString, crate::table::TableKeyValue>,
+    span: Option<std::ops::Range<usize>>,
     value: Option<(crate::InternalString, crate::Item)>,
 }
 
 impl TableMapAccess {
     pub(crate) fn new(input: crate::Table) -> Self {
+        let span = input.span();
         Self {
             iter: input.items.into_iter(),
+            span,
             value: None,
         }
     }
@@ -105,7 +110,14 @@ impl<'de> serde::de::MapAccess<'de> for TableMapAccess {
     {
         match self.iter.next() {
             Some((k, v)) => {
-                let ret = seed.deserialize(k.into_deserializer()).map(Some);
+                let ret = seed.deserialize(k.into_deserializer()).map(Some).map_err(
+                    |mut e: Self::Error| {
+                        if e.span().is_none() {
+                            e.set_span(v.key.span());
+                        }
+                        e
+                    },
+                );
                 self.value = Some((k, v.value));
                 ret
             }
@@ -118,12 +130,7 @@ impl<'de> serde::de::MapAccess<'de> for TableMapAccess {
         V: serde::de::DeserializeSeed<'de>,
     {
         match self.value.take() {
-            Some((k, v)) => seed
-                .deserialize(crate::de::ItemDeserializer::new(v))
-                .map_err(|mut err| {
-                    err.parent_key(k);
-                    err
-                }),
+            Some((_k, v)) => seed.deserialize(crate::de::ItemDeserializer::new(v)),
             None => {
                 panic!("no more values in next_value_seed, internal error in ItemDeserializer")
             }
@@ -144,6 +151,7 @@ impl<'de> serde::de::EnumAccess<'de> for TableMapAccess {
             None => {
                 return Err(Error::custom(
                     "expected table with exactly 1 entry, found empty table",
+                    self.span,
                 ));
             }
         };
diff --git a/crates/toml_edit/src/de/table_enum.rs b/crates/toml_edit/src/de/table_enum.rs
index bb4d94fa..bea79d41 100644
--- a/crates/toml_edit/src/de/table_enum.rs
+++ b/crates/toml_edit/src/de/table_enum.rs
@@ -20,20 +20,20 @@ impl<'de> serde::de::VariantAccess<'de> for TableEnumDeserializer {
                 if values.is_empty() {
                     Ok(())
                 } else {
-                    Err(Error::custom("expected empty table"))
+                    Err(Error::custom("expected empty table", values.span()))
                 }
             }
             crate::Item::Value(crate::Value::InlineTable(values)) => {
                 if values.is_empty() {
                     Ok(())
                 } else {
-                    Err(Error::custom("expected empty table"))
+                    Err(Error::custom("expected empty table", values.span()))
                 }
             }
-            e => Err(Error::custom(format!(
-                "expected table, found {}",
-                e.type_name()
-            ))),
+            e => Err(Error::custom(
+                format!("expected table, found {}", e.type_name()),
+                e.span(),
+            )),
         }
     }
 
@@ -50,16 +50,24 @@ impl<'de> serde::de::VariantAccess<'de> for TableEnumDeserializer {
     {
         match self.value {
             crate::Item::Table(values) => {
+                let values_span = values.span();
                 let tuple_values = values
+                    .items
                     .into_iter()
                     .enumerate()
-                    .map(|(index, (key, value))| match key.parse::<usize>() {
-                        Ok(key_index) if key_index == index => Ok(value),
-                        Ok(_) | Err(_) => Err(Error::custom(format!(
-                            "expected table key `{}`, but was `{}`",
-                            index, key
-                        ))),
-                    })
+                    .map(
+                        |(index, (_, value))| match value.key.get().parse::<usize>() {
+                            Ok(key_index) if key_index == index => Ok(value.value),
+                            Ok(_) | Err(_) => Err(Error::custom(
+                                format!(
+                                    "expected table key `{}`, but was `{}`",
+                                    index,
+                                    value.key.get()
+                                ),
+                                value.key.span(),
+                            )),
+                        },
+                    )
                     // Fold all values into a `Vec`, or return the first error.
                     .fold(Ok(Vec::with_capacity(len)), |result, value_result| {
                         result.and_then(move |mut tuple_values| match value_result {
@@ -78,25 +86,36 @@ impl<'de> serde::de::VariantAccess<'de> for TableEnumDeserializer {
                         visitor,
                     )
                 } else {
-                    Err(Error::custom(format!("expected tuple with length {}", len)))
+                    Err(Error::custom(
+                        format!("expected tuple with length {}", len),
+                        values_span,
+                    ))
                 }
             }
             crate::Item::Value(crate::Value::InlineTable(values)) => {
+                let values_span = values.span();
                 let tuple_values = values
+                    .items
                     .into_iter()
                     .enumerate()
-                    .map(|(index, (key, value))| match key.parse::<usize>() {
-                        Ok(key_index) if key_index == index => Ok(value),
-                        Ok(_) | Err(_) => Err(Error::custom(format!(
-                            "expected table key `{}`, but was `{}`",
-                            index, key
-                        ))),
-                    })
+                    .map(
+                        |(index, (_, value))| match value.key.get().parse::<usize>() {
+                            Ok(key_index) if key_index == index => Ok(value.value),
+                            Ok(_) | Err(_) => Err(Error::custom(
+                                format!(
+                                    "expected table key `{}`, but was `{}`",
+                                    index,
+                                    value.key.get()
+                                ),
+                                value.key.span(),
+                            )),
+                        },
+                    )
                     // Fold all values into a `Vec`, or return the first error.
                     .fold(Ok(Vec::with_capacity(len)), |result, value_result| {
                         result.and_then(move |mut tuple_values| match value_result {
                             Ok(value) => {
-                                tuple_values.push(crate::Item::Value(value));
+                                tuple_values.push(value);
                                 Ok(tuple_values)
                             }
                             // `Result<de::Value, Self::Error>` to `Result<Vec<_>, Self::Error>`
@@ -110,13 +129,16 @@ impl<'de> serde::de::VariantAccess<'de> for TableEnumDeserializer {
                         visitor,
                     )
                 } else {
-                    Err(Error::custom(format!("expected tuple with length {}", len)))
+                    Err(Error::custom(
+                        format!("expected tuple with length {}", len),
+                        values_span,
+                    ))
                 }
             }
-            e => Err(Error::custom(format!(
-                "expected table, found {}",
-                e.type_name()
-            ))),
+            e => Err(Error::custom(
+                format!("expected table, found {}", e.type_name()),
+                e.span(),
+            )),
         }
     }
 
diff --git a/crates/toml_edit/src/de/value.rs b/crates/toml_edit/src/de/value.rs
index ff169301..f24bc54c 100644
--- a/crates/toml_edit/src/de/value.rs
+++ b/crates/toml_edit/src/de/value.rs
@@ -71,16 +71,21 @@ impl<'de> serde::Deserializer<'de> for crate::Value {
                 if v.is_empty() {
                     Err(crate::de::Error::custom(
                         "wanted exactly 1 element, found 0 elements",
+                        v.span(),
                     ))
                 } else if v.len() != 1 {
                     Err(crate::de::Error::custom(
                         "wanted exactly 1 element, more than 1 element",
+                        v.span(),
                     ))
                 } else {
                     visitor.visit_enum(crate::de::InlineTableMapAccess::new(v))
                 }
             }
-            _ => Err(crate::de::Error::custom("wanted string or inline table")),
+            _ => Err(crate::de::Error::custom(
+                "wanted string or inline table",
+                self.span(),
+            )),
         }
     }
 
diff --git a/crates/toml_edit/src/inline_table.rs b/crates/toml_edit/src/inline_table.rs
index 7f2424cf..0203d932 100644
--- a/crates/toml_edit/src/inline_table.rs
+++ b/crates/toml_edit/src/inline_table.rs
@@ -173,6 +173,12 @@ impl InlineTable {
         &self.preamble
     }
 
+    /// Returns the location within the original document
+    #[cfg(feature = "serde")]
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        self.span.clone()
+    }
+
     pub(crate) fn despan(&mut self, input: &str) {
         self.span = None;
         self.decor.despan(input);
diff --git a/crates/toml_edit/src/item.rs b/crates/toml_edit/src/item.rs
index 972e973b..8a934ebe 100644
--- a/crates/toml_edit/src/item.rs
+++ b/crates/toml_edit/src/item.rs
@@ -296,6 +296,17 @@ impl Item {
         self.as_table_like().is_some()
     }
 
+    /// Returns the location within the original document
+    #[cfg(feature = "serde")]
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        match self {
+            Item::None => None,
+            Item::Value(v) => v.span(),
+            Item::Table(_) => None,
+            Item::ArrayOfTables(_) => None,
+        }
+    }
+
     pub(crate) fn despan(&mut self, input: &str) {
         match self {
             Item::None => {}
diff --git a/crates/toml_edit/src/key.rs b/crates/toml_edit/src/key.rs
index fa75d3c1..35beb3e4 100644
--- a/crates/toml_edit/src/key.rs
+++ b/crates/toml_edit/src/key.rs
@@ -108,6 +108,11 @@ impl Key {
         &self.decor
     }
 
+    /// Returns the location within the original document
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        self.repr.as_ref().and_then(|r| r.span())
+    }
+
     pub(crate) fn despan(&mut self, input: &str) {
         self.decor.despan(input);
         if let Some(repr) = &mut self.repr {
diff --git a/crates/toml_edit/src/parser/errors.rs b/crates/toml_edit/src/parser/errors.rs
index 81e7ef52..31bc0dcb 100644
--- a/crates/toml_edit/src/parser/errors.rs
+++ b/crates/toml_edit/src/parser/errors.rs
@@ -10,37 +10,68 @@ use crate::Key;
 #[derive(Debug, Clone, Eq, PartialEq, Hash)]
 pub struct TomlError {
     message: String,
-    line_col: Option<(usize, usize)>,
+    original: Option<String>,
+    span: Option<std::ops::Range<usize>>,
 }
 
 impl TomlError {
     pub(crate) fn new(error: ParserError<'_>, original: Input<'_>) -> Self {
+        use nom8::input::IntoOutput;
         use nom8::input::Offset;
+
         let offset = original.offset(&error.input);
-        let position = translate_position(&original, offset);
-        let message = ParserErrorDisplay {
-            error: &error,
-            original,
-            position,
+        let span = if offset == original.len() {
+            offset..offset
+        } else {
+            offset..(offset + 1)
+        };
+
+        let message = error.to_string();
+
+        Self {
+            message,
+            original: Some(
+                String::from_utf8(original.into_output().to_owned())
+                    .expect("original document was utf8"),
+            ),
+            span: Some(span),
         }
-        .to_string();
-        let line_col = Some(position);
-        Self { message, line_col }
     }
 
     #[cfg(feature = "serde")]
-    pub(crate) fn custom(message: String) -> Self {
+    pub(crate) fn custom(message: String, span: Option<std::ops::Range<usize>>) -> Self {
         Self {
             message,
-            line_col: None,
+            original: None,
+            span,
         }
     }
 
+    /// The start/end index into the original document where the error occurred
+    pub fn span(&self) -> Option<std::ops::Range<usize>> {
+        self.span.clone()
+    }
+
+    #[cfg(feature = "serde")]
+    pub(crate) fn set_span(&mut self, span: Option<std::ops::Range<usize>>) {
+        self.span = span;
+    }
+
+    #[cfg(feature = "serde")]
+    pub(crate) fn set_original(&mut self, original: Option<String>) {
+        self.original = original;
+    }
+
     /// Produces a (line, column) pair of the position of the error if available
     ///
     /// All indexes are 0-based.
+    #[deprecated(since = "0.18.0", note = "See instead `TomlError::span`")]
     pub fn line_col(&self) -> Option<(usize, usize)> {
-        self.line_col
+        if let (Some(original), Some(span)) = (&self.original, self.span()) {
+            Some(translate_position(original.as_bytes(), span.start))
+        } else {
+            None
+        }
     }
 }
 
@@ -58,7 +89,47 @@ impl TomlError {
 /// While parsing a Date-Time
 impl Display for TomlError {
     fn fmt(&self, f: &mut Formatter<'_>) -> Result {
-        write!(f, "{}", self.message)
+        if let (Some(original), Some(span)) = (&self.original, self.span()) {
+            let (line, column) = translate_position(original.as_bytes(), span.start);
+            let line_num = line + 1;
+            let col_num = column + 1;
+            let gutter = line_num.to_string().len();
+            let content = original.split('\n').nth(line).expect("valid line number");
+
+            writeln!(
+                f,
+                "TOML parse error at line {}, column {}",
+                line_num, col_num
+            )?;
+            //   |
+            for _ in 0..=gutter {
+                write!(f, " ")?;
+            }
+            writeln!(f, "|")?;
+
+            // 1 | 00:32:00.a999999
+            write!(f, "{} | ", line_num)?;
+            writeln!(f, "{}", content)?;
+
+            //   |          ^
+            for _ in 0..=gutter {
+                write!(f, " ")?;
+            }
+            write!(f, "|")?;
+            for _ in 0..=column {
+                write!(f, " ")?;
+            }
+            // The span will be empty at eof, so we need to make sure we always print at least
+            // one `^`
+            write!(f, "^")?;
+            for _ in (span.start + 1)..(span.end.min(span.start + content.len())) {
+                write!(f, "^")?;
+            }
+            writeln!(f)?;
+        }
+        write!(f, "{}", self.message)?;
+
+        Ok(())
     }
 }
 
@@ -126,31 +197,13 @@ impl<'b> std::cmp::PartialEq for ParserError<'b> {
     }
 }
 
-struct ParserErrorDisplay<'a> {
-    error: &'a ParserError<'a>,
-    original: Input<'a>,
-    position: (usize, usize),
-}
-
-impl<'a> std::fmt::Display for ParserErrorDisplay<'a> {
+impl<'a> std::fmt::Display for ParserError<'a> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let (line, column) = self.position;
-        let line_num = line + 1;
-        let col_num = column + 1;
-        let gutter = line_num.to_string().len();
-        let content = self
-            .original
-            .split(|b| *b == b'\n')
-            .nth(line)
-            .expect("valid line number");
-        let content = String::from_utf8_lossy(content);
-
-        let expression = self.error.context.iter().find_map(|c| match c {
+        let expression = self.context.iter().find_map(|c| match c {
             Context::Expression(c) => Some(c),
             _ => None,
         });
         let expected = self
-            .error
             .context
             .iter()
             .filter_map(|c| match c {
@@ -159,31 +212,6 @@ impl<'a> std::fmt::Display for ParserErrorDisplay<'a> {
             })
             .collect::<Vec<_>>();
 
-        writeln!(
-            f,
-            "TOML parse error at line {}, column {}",
-            line_num, col_num
-        )?;
-        //   |
-        for _ in 0..=gutter {
-            write!(f, " ")?;
-        }
-        writeln!(f, "|")?;
-
-        // 1 | 00:32:00.a999999
-        write!(f, "{} | ", line_num)?;
-        writeln!(f, "{}", content)?;
-
-        //   |          ^
-        for _ in 0..=gutter {
-            write!(f, " ")?;
-        }
-        write!(f, "|")?;
-        for _ in 0..=column {
-            write!(f, " ")?;
-        }
-        writeln!(f, "^")?;
-
         if let Some(expression) = expression {
             writeln!(f, "Invalid {}", expression)?;
         }
@@ -198,7 +226,7 @@ impl<'a> std::fmt::Display for ParserErrorDisplay<'a> {
             }
             writeln!(f)?;
         }
-        if let Some(cause) = &self.error.cause {
+        if let Some(cause) = &self.cause {
             write!(f, "{}", cause)?;
         }
 
diff --git a/crates/toml_edit/src/parser/state.rs b/crates/toml_edit/src/parser/state.rs
index 45a4845a..d55ff5b6 100644
--- a/crates/toml_edit/src/parser/state.rs
+++ b/crates/toml_edit/src/parser/state.rs
@@ -114,6 +114,7 @@ impl ParseState {
         self.current_table_position += 1;
         self.current_table.decor = decor;
         self.current_table.set_position(self.current_table_position);
+        self.current_table.span = path.last().unwrap().span();
         self.current_is_array = true;
         self.current_table_path = path;
 
@@ -143,6 +144,7 @@ impl ParseState {
         self.current_table_position += 1;
         self.current_table.decor = decor;
         self.current_table.set_position(self.current_table_position);
+        self.current_table.span = path.last().unwrap().span();
         self.current_is_array = false;
         self.current_table_path = path;
 
diff --git a/crates/toml_edit/src/repr.rs b/crates/toml_edit/src/repr.rs
index 56e39530..39205960 100644
--- a/crates/toml_edit/src/repr.rs
+++ b/crates/toml_edit/src/repr.rs
@@ -58,6 +58,12 @@ where
             })
     }
 
+    /// Returns the location within the original document
+    #[cfg(feature = "serde")]
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        self.repr.as_ref().and_then(|r| r.span())
+    }
+
     pub(crate) fn despan(&mut self, input: &str) {
         self.decor.despan(input);
         if let Some(repr) = &mut self.repr {
@@ -113,6 +119,11 @@ impl Repr {
         &self.raw_value
     }
 
+    /// Returns the location within the original document
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        self.raw_value.span()
+    }
+
     pub(crate) fn despan(&mut self, input: &str) {
         self.raw_value.despan(input)
     }
diff --git a/crates/toml_edit/src/ser/mod.rs b/crates/toml_edit/src/ser/mod.rs
index 86ea5625..497da194 100644
--- a/crates/toml_edit/src/ser/mod.rs
+++ b/crates/toml_edit/src/ser/mod.rs
@@ -55,7 +55,7 @@ impl From<crate::TomlError> for Error {
 
 impl From<Error> for crate::TomlError {
     fn from(e: Error) -> crate::TomlError {
-        Self::custom(e.to_string())
+        Self::custom(e.to_string(), None)
     }
 }
 
diff --git a/crates/toml_edit/src/table.rs b/crates/toml_edit/src/table.rs
index f5994c57..f9eedc5e 100644
--- a/crates/toml_edit/src/table.rs
+++ b/crates/toml_edit/src/table.rs
@@ -21,6 +21,7 @@ pub struct Table {
     // `None` for user created tables (can be overridden with `set_position`)
     doc_position: Option<usize>,
     pub(crate) items: KeyValuePairs,
+    pub(crate) span: Option<std::ops::Range<usize>>,
 }
 
 /// Constructors
@@ -223,7 +224,14 @@ impl Table {
         self.items.get(key).map(|kv| &kv.key.decor)
     }
 
+    /// Returns the location within the original document
+    #[cfg(feature = "serde")]
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        self.span.clone()
+    }
+
     pub(crate) fn despan(&mut self, input: &str) {
+        self.span = None;
         for kv in self.items.values_mut() {
             kv.key.despan(input);
             kv.value.despan(input);
diff --git a/crates/toml_edit/src/value.rs b/crates/toml_edit/src/value.rs
index 5f6b9a9c..110c258e 100644
--- a/crates/toml_edit/src/value.rs
+++ b/crates/toml_edit/src/value.rs
@@ -205,6 +205,20 @@ impl Value {
         *decor = Decor::new(prefix, suffix);
     }
 
+    /// Returns the location within the original document
+    #[cfg(feature = "serde")]
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        match self {
+            Value::String(f) => f.span(),
+            Value::Integer(f) => f.span(),
+            Value::Float(f) => f.span(),
+            Value::Boolean(f) => f.span(),
+            Value::Datetime(f) => f.span(),
+            Value::Array(a) => a.span(),
+            Value::InlineTable(t) => t.span(),
+        }
+    }
+
     pub(crate) fn despan(&mut self, input: &str) {
         match self {
             Value::String(f) => f.despan(input),


Test patch:
diff --git a/crates/toml_edit/tests/testsuite/enum_external_deserialize.rs b/crates/toml_edit/tests/testsuite/enum_external_deserialize.rs
index 5321ffe4..c68a39c5 100644
--- a/crates/toml_edit/tests/testsuite/enum_external_deserialize.rs
+++ b/crates/toml_edit/tests/testsuite/enum_external_deserialize.rs
@@ -30,18 +30,26 @@ struct Multi {
 fn invalid_variant_returns_error_with_good_message_string() {
     let error = toml::from_str::<Val>("val = \"NonExistent\"").unwrap_err();
 
-    assert_eq!(
+    snapbox::assert_eq(
         error.to_string(),
-        "unknown variant `NonExistent`, expected one of `Plain`, `Tuple`, `NewType`, `Struct` for key `val`"
+        r#"TOML parse error at line 1, column 7
+  |
+1 | val = "NonExistent"
+  |       ^^^^^^^^^^^^^
+unknown variant `NonExistent`, expected one of `Plain`, `Tuple`, `NewType`, `Struct`"#,
     );
 }
 
 #[test]
 fn invalid_variant_returns_error_with_good_message_inline_table() {
     let error = toml::from_str::<Val>("val = { NonExistent = {} }").unwrap_err();
-    assert_eq!(
+    snapbox::assert_eq(
         error.to_string(),
-        "unknown variant `NonExistent`, expected one of `Plain`, `Tuple`, `NewType`, `Struct` for key `val`"
+        r#"TOML parse error at line 1, column 9
+  |
+1 | val = { NonExistent = {} }
+  |         ^^^^^^^^^^^
+unknown variant `NonExistent`, expected one of `Plain`, `Tuple`, `NewType`, `Struct`"#,
     );
 }
 
@@ -49,7 +57,14 @@ fn invalid_variant_returns_error_with_good_message_inline_table() {
 fn extra_field_returns_expected_empty_table_error() {
     let error = toml::from_str::<Val>("val = { Plain = { extra_field = 404 } }").unwrap_err();
 
-    assert_eq!(error.to_string(), "expected empty table for key `val`");
+    snapbox::assert_eq(
+        error.to_string(),
+        r#"TOML parse error at line 1, column 17
+  |
+1 | val = { Plain = { extra_field = 404 } }
+  |                 ^^^^^^^^^^^^^^^^^^^^^
+expected empty table"#,
+    );
 }
 
 #[test]
@@ -58,9 +73,13 @@ fn extra_field_returns_expected_empty_table_error_struct_variant() {
         toml::from_str::<Val>("val = { Struct = { value = 123, extra_0 = 0, extra_1 = 1 } }")
             .unwrap_err();
 
-    assert_eq!(
+    snapbox::assert_eq(
         error.to_string(),
-        r#"unexpected keys in table: extra_0, extra_1, available keys: value for key `val`"#
+        r#"TOML parse error at line 1, column 33
+  |
+1 | val = { Struct = { value = 123, extra_0 = 0, extra_1 = 1 } }
+  |                                 ^^^^^^^
+unexpected keys in table: extra_0, extra_1, available keys: value"#,
     );
 }
 
diff --git a/crates/toml_edit/tests/testsuite/serde.rs b/crates/toml_edit/tests/testsuite/serde.rs
index 1102d024..b4d9dfd7 100644
--- a/crates/toml_edit/tests/testsuite/serde.rs
+++ b/crates/toml_edit/tests/testsuite/serde.rs
@@ -30,14 +30,14 @@ macro_rules! equivalent {
 
         // Through a string equivalent
         println!("to_string(literal)");
-        assert_eq!(
+        snapbox::assert_eq(
             t!(toml_edit::easy::to_string_pretty(&literal)),
-            toml.to_string()
+            toml.to_string(),
         );
         println!("to_string(toml)");
-        assert_eq!(
+        snapbox::assert_eq(
             t!(toml_edit::easy::to_string_pretty(&toml)),
-            toml.to_string()
+            toml.to_string(),
         );
         println!("literal, from_str(toml)");
         assert_eq!(literal, t!(toml_edit::easy::from_str(&toml.to_string())));
@@ -51,13 +51,13 @@ macro_rules! error {
         println!("attempting parsing");
         match toml_edit::easy::from_str::<$ty>(&$toml.to_string()) {
             Ok(_) => panic!("successful"),
-            Err(e) => assert_eq!(e.to_string(), $msg_parse),
+            Err(e) => snapbox::assert_eq(e.to_string(), $msg_parse),
         }
 
         println!("attempting toml decoding");
         match $toml.try_into::<$ty>() {
             Ok(_) => panic!("successful"),
-            Err(e) => assert_eq!(e.to_string(), $msg_decode),
+            Err(e) => snapbox::assert_eq(e.to_string(), $msg_decode),
         }
     }};
 }
@@ -218,8 +218,12 @@ fn type_errors() {
         Table(map! {
             bar: Value::String("a".to_string())
         }),
-        "invalid type: string \"a\", expected isize for key `bar`",
-        "invalid type: string \"a\", expected isize for key `bar`"
+        r#"TOML parse error at line 1, column 7
+  |
+1 | bar = "a"
+  |       ^^^
+invalid type: string "a", expected isize"#,
+        "invalid type: string \"a\", expected isize"
     }
 
     #[derive(Deserialize)]
@@ -235,8 +239,12 @@ fn type_errors() {
                 bar: Value::String("a".to_string())
             })
         }),
-        "invalid type: string \"a\", expected isize for key `foo.bar`",
-        "invalid type: string \"a\", expected isize for key `foo.bar`"
+        r#"TOML parse error at line 2, column 7
+  |
+2 | bar = "a"
+  |       ^^^
+invalid type: string "a", expected isize"#,
+        "invalid type: string \"a\", expected isize"
     }
 }
 
@@ -442,7 +450,7 @@ fn table_structs_empty() {
     );
     expected.insert("foo".to_string(), CanBeEmpty::default());
     assert_eq!(value, expected);
-    assert_eq!(toml_edit::ser::to_string(&value).unwrap(), text);
+    snapbox::assert_eq(toml_edit::ser::to_string(&value).unwrap(), text);
 }
 
 #[test]
@@ -536,10 +544,7 @@ debug = 'a'
 "#,
     );
     let err = res.unwrap_err();
-    assert_eq!(
-        err.to_string(),
-        "expected a boolean or an integer for key `profile.dev.debug`"
-    );
+    snapbox::assert_eq(err.to_string(), "expected a boolean or an integer");
 
     let res: Result<Package, _> = toml_edit::de::from_str(
         r#"
@@ -553,9 +558,13 @@ dev = { debug = 'a' }
 "#,
     );
     let err = res.unwrap_err();
-    assert_eq!(
+    snapbox::assert_eq(
         err.to_string(),
-        "expected a boolean or an integer for key `profile.dev.debug`"
+        r#"TOML parse error at line 8, column 17
+  |
+8 | dev = { debug = 'a' }
+  |                 ^^^
+expected a boolean or an integer"#,
     );
 }