feat: keep span info when config parsing fails (#3727)

This commit is contained in:
三咲雅 misaki masa 2026-03-02 10:03:14 +08:00 committed by sxyazi
parent 3cdc3ecb70
commit 0a83f0bd27
No known key found for this signature in database
12 changed files with 141 additions and 173 deletions

16
Cargo.lock generated
View file

@ -2319,11 +2319,10 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.12" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [ dependencies = [
"bitflags 2.11.0",
"libc", "libc",
] ]
@ -3321,12 +3320,9 @@ dependencies = [
[[package]] [[package]]
name = "pxfm" name = "pxfm"
version = "0.1.27" version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "qoi" name = "qoi"
@ -3746,9 +3742,9 @@ dependencies = [
[[package]] [[package]]
name = "russh" name = "russh"
version = "0.57.0" version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01fe22d10a0e39c1134a971d5b8db8a40357b48ef22d81fa8d6eac22202dd782" checksum = "afe62631a04a1f4d71a14b99505483b95ff97c503b67d876c042fce659186956"
dependencies = [ dependencies = [
"aes", "aes",
"bitflags 2.11.0", "bitflags 2.11.0",

View file

@ -52,7 +52,7 @@ percent-encoding = "2.3.2"
rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] } rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] }
ratatui = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] } ratatui = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] }
regex = "1.12.3" regex = "1.12.3"
russh = { version = "0.57.0", default-features = false, features = [ "ring", "rsa" ] } russh = { version = "0.57.1", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] } serde = { version = "1.0.228", features = [ "derive" ] }
serde_json = "1.0.149" serde_json = "1.0.149"

View file

@ -1,40 +1,50 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{format_ident, quote}; use quote::quote;
use syn::{Attribute, Data, DeriveInput, Fields, parse_macro_input}; use syn::{Data, DeriveInput, Fields, parse_macro_input};
#[proc_macro_derive(DeserializeOver)]
pub fn deserialize_over(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input as DeriveInput);
quote! {
impl #ident {
pub(crate) fn deserialize_over(self, input: &str) -> Result<Self, toml::de::Error> {
crate::error_with_input(self.deserialize_over_with(toml::de::DeTable::parse(input)?), input)
}
}
}
.into()
}
#[proc_macro_derive(DeserializeOver1)] #[proc_macro_derive(DeserializeOver1)]
pub fn deserialize_over1(input: TokenStream) -> TokenStream { pub fn deserialize_over1(input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
let input = parse_macro_input!(input as DeriveInput);
// Get the name of the struct let assignments = match data {
let name = &input.ident; Data::Struct(struct_) => match struct_.fields {
let shadow_name = format_ident!("__{name}Shadow");
// Process the struct fields
let (shadow_fields, field_calls) = match &input.data {
Data::Struct(struct_) => match &struct_.fields {
Fields::Named(fields) => { Fields::Named(fields) => {
let mut shadow_fields = Vec::with_capacity(fields.named.len()); let mut assignments = Vec::with_capacity(fields.named.len());
let mut field_calls = Vec::with_capacity(fields.named.len());
for field in &fields.named { for field in fields.named {
let name = &field.ident; let field_ident = &field.ident;
let attrs: Vec<&Attribute> = let field_name = field_ident.as_ref().unwrap().to_string();
field.attrs.iter().filter(|&a| a.path().is_ident("serde")).collect();
shadow_fields.push(quote! { assignments.push(quote! {
#(#attrs)* if let Some(value) = table.remove(#field_name) {
pub(crate) #name: Option<toml::Value> if !matches!(value.get_ref(), toml::de::DeValue::Table(_)) {
}); _ = toml::Table::deserialize(value.into_deserializer())?;
field_calls.push(quote! { return Err(serde::de::Error::custom(format!("expected top-level `{}` to be a TOML table", #field_name)));
if let Some(value) = shadow.#name { }
self.#name = self.#name.deserialize_over(value).map_err(serde::de::Error::custom)?;
let span = value.span();
if let toml::de::DeValue::Table(table) = value.into_inner() {
self.#field_ident = self.#field_ident.deserialize_over_with(toml::Spanned::new(span, table))?;
}
} }
}); });
} }
(shadow_fields, field_calls) assignments
} }
_ => panic!("DeserializeOver1 only supports structs with named fields"), _ => panic!("DeserializeOver1 only supports structs with named fields"),
}, },
@ -42,34 +52,14 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
}; };
quote! { quote! {
#[derive(serde::Deserialize)] impl #ident {
pub(crate) struct #shadow_name {
#(#shadow_fields),*
}
impl #name {
#[inline] #[inline]
pub(crate) fn deserialize_over<'de, D>(self, deserializer: D) -> Result<Self, D::Error> pub(crate) fn deserialize_over_with<'de>(mut self, table: toml::Spanned<toml::de::DeTable<'de>>) -> Result<Self, toml::de::Error> {
where use serde::{Deserialize, de::IntoDeserializer};
D: serde::Deserializer<'de>,
{
self.deserialize_over_with::<D>(Self::deserialize_shadow(deserializer)?)
}
#[inline] let mut table = table.into_inner();
pub(crate) fn deserialize_shadow<'de, D>(deserializer: D) -> Result<#shadow_name, D::Error> #(#assignments)*
where
D: serde::Deserializer<'de>,
{
#shadow_name::deserialize(deserializer)
}
#[inline]
pub(crate) fn deserialize_over_with<'de, D>(mut self, shadow: #shadow_name) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#(#field_calls)*
Ok(self) Ok(self)
} }
} }
@ -79,33 +69,25 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
#[proc_macro_derive(DeserializeOver2)] #[proc_macro_derive(DeserializeOver2)]
pub fn deserialize_over2(input: TokenStream) -> TokenStream { pub fn deserialize_over2(input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
let input = parse_macro_input!(input as DeriveInput);
// Get the name of the struct let assignments = match data {
let name = &input.ident; Data::Struct(struct_) => match struct_.fields {
let shadow_name = format_ident!("__{name}Shadow");
// Process the struct fields
let (shadow_fields, field_assignments) = match &input.data {
Data::Struct(struct_) => match &struct_.fields {
Fields::Named(fields) => { Fields::Named(fields) => {
let mut shadow_fields = Vec::with_capacity(fields.named.len()); let mut assignments = Vec::with_capacity(fields.named.len());
let mut field_assignments = Vec::with_capacity(fields.named.len());
for field in &fields.named { for field in fields.named {
let (ty, name) = (&field.ty, &field.ident); let field_ident = field.ident;
shadow_fields.push(quote! { let field_name = field_ident.as_ref().unwrap().to_string();
pub(crate) #name: Option<#ty>
}); assignments.push(quote! {
field_assignments.push(quote! { if let Some(value) = table.remove(#field_name) {
if let Some(value) = shadow.#name { self.#field_ident = <_>::deserialize(value.into_deserializer())?;
self.#name = value;
} }
}); });
} }
(shadow_fields, field_assignments) assignments
} }
_ => panic!("DeserializeOver2 only supports structs with named fields"), _ => panic!("DeserializeOver2 only supports structs with named fields"),
}, },
@ -113,32 +95,15 @@ pub fn deserialize_over2(input: TokenStream) -> TokenStream {
}; };
quote! { quote! {
#[derive(serde::Deserialize)] impl #ident {
pub(crate) struct #shadow_name {
#(#shadow_fields),*
}
impl #name {
#[inline] #[inline]
pub(crate) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result<Self, D::Error> pub(crate) fn deserialize_over_with<'de>(mut self, table: toml::Spanned<toml::de::DeTable<'de>>) -> Result<Self, toml::de::Error> {
where use serde::{Deserialize, de::IntoDeserializer};
D: serde::Deserializer<'de>
{
Ok(self.deserialize_over_with(Self::deserialize_shadow(deserializer)?))
}
#[inline] let mut table = table.into_inner();
pub(crate) fn deserialize_shadow<'de, D>(deserializer: D) -> Result<#shadow_name, D::Error> #(#assignments)*
where
D: serde::Deserializer<'de>
{
#shadow_name::deserialize(deserializer)
}
#[inline] Ok(self)
pub(crate) fn deserialize_over_with(mut self, shadow: #shadow_name) -> Self {
#(#field_assignments)*
self
} }
} }
} }

View file

@ -1,12 +1,12 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;
use yazi_codegen::DeserializeOver1; use yazi_codegen::{DeserializeOver, DeserializeOver1};
use yazi_fs::{Xdg, ok_or_not_found}; use yazi_fs::{Xdg, ok_or_not_found};
use yazi_shared::Layer; use yazi_shared::Layer;
use super::{Chord, KeymapRules}; use super::{Chord, KeymapRules};
#[derive(Deserialize, DeserializeOver1)] #[derive(Deserialize, DeserializeOver, DeserializeOver1)]
pub struct Keymap { pub struct Keymap {
pub mgr: KeymapRules, pub mgr: KeymapRules,
pub tasks: KeymapRules, pub tasks: KeymapRules,

View file

@ -25,8 +25,8 @@ fn try_init(merge: bool) -> anyhow::Result<()> {
let mut keymap = Preset::keymap()?; let mut keymap = Preset::keymap()?;
if merge { if merge {
yazi = yazi.deserialize_over(toml::Deserializer::parse(&yazi::Yazi::read()?)?)?; yazi = yazi.deserialize_over(&yazi::Yazi::read()?)?;
keymap = keymap.deserialize_over(toml::Deserializer::parse(&keymap::Keymap::read()?)?)?; keymap = keymap.deserialize_over(&keymap::Keymap::read()?)?;
} }
YAZI.init(yazi.reshape()?); YAZI.init(yazi.reshape()?);
@ -43,17 +43,19 @@ pub fn init_flavor(light: bool) -> anyhow::Result<()> {
} }
fn try_init_flavor(light: bool, merge: bool) -> anyhow::Result<()> { fn try_init_flavor(light: bool, merge: bool) -> anyhow::Result<()> {
let mut theme = Preset::theme(light)?; let mut preset = Preset::theme(light)?;
if merge { if merge {
let shadow = let theme_str = theme::Theme::read()?;
theme::Theme::deserialize_shadow(toml::Deserializer::parse(&theme::Theme::read()?)?)?; let theme = toml::de::DeTable::parse(&theme_str)?;
let flavor = shadow.flavor.as_ref().map(theme::Flavor::from).unwrap_or_default().read(light)?;
theme = theme.deserialize_over(toml::Deserializer::parse(&flavor)?)?; let flavor_str = theme::Flavor::from_theme(&theme, &theme_str)?.read(light)?;
theme = theme.deserialize_over_with::<toml::Value>(shadow)?;
preset = preset.deserialize_over(&flavor_str)?;
preset = error_with_input(preset.deserialize_over_with(theme), &theme_str)?;
} }
THEME.init(theme.reshape(light)?); THEME.init(preset.reshape(light)?);
Ok(()) Ok(())
} }
@ -78,3 +80,13 @@ fn wait_for_key(e: anyhow::Error) -> anyhow::Result<()> {
TTY.reader().read_exact(&mut [0])?; TTY.reader().read_exact(&mut [0])?;
Ok(()) Ok(())
} }
pub(crate) fn error_with_input<T>(
result: Result<T, toml::de::Error>,
input: &str,
) -> Result<T, toml::de::Error> {
result.map_err(|mut err| {
err.set_input(Some(input));
err
})
}

View file

@ -3,12 +3,14 @@ use std::{mem, ops::Deref};
use anyhow::Result; use anyhow::Result;
use hashbrown::HashMap; use hashbrown::HashMap;
use indexmap::IndexSet; use indexmap::IndexSet;
use serde::Deserialize; use serde::{Deserialize, de::IntoDeserializer};
use toml::{Spanned, de::DeTable};
use yazi_codegen::DeserializeOver;
use super::OpenerRule; use super::OpenerRule;
use crate::check_for; use crate::check_for;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, DeserializeOver)]
pub struct Opener(HashMap<String, Vec<OpenerRule>>); pub struct Opener(HashMap<String, Vec<OpenerRule>>);
impl Deref for Opener { impl Deref for Opener {
@ -56,12 +58,13 @@ impl Opener {
Ok(self) Ok(self)
} }
pub(crate) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result<Self, D::Error> pub(crate) fn deserialize_over_with<'de>(
where mut self,
D: serde::Deserializer<'de>, table: Spanned<DeTable<'de>>,
{ ) -> Result<Self, toml::de::Error> {
let map: HashMap<String, Vec<OpenerRule>> = HashMap::deserialize(deserializer)?; for (key, value) in table.into_inner() {
self.0.extend(map); self.0.insert(key.into_inner().into_owned(), <_>::deserialize(value.into_deserializer())?);
}
Ok(self) Ok(self)
} }

View file

@ -1,33 +1,30 @@
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize, de::IntoDeserializer};
use toml::Value; use toml::{Spanned, de::DeTable};
use yazi_codegen::DeserializeOver2; use yazi_codegen::DeserializeOver2;
use yazi_fs::Xdg; use yazi_fs::Xdg;
use crate::error_with_input;
#[derive(Default, Deserialize, DeserializeOver2, Serialize)] #[derive(Default, Deserialize, DeserializeOver2, Serialize)]
pub struct Flavor { pub struct Flavor {
#[serde(default)]
pub dark: String, pub dark: String,
#[serde(default)]
pub light: String, pub light: String,
} }
impl From<&Value> for Flavor {
fn from(value: &Value) -> Self {
let mut me = Self::default();
if let Value::Table(t) = value {
if let Some(s) = t.get("dark").and_then(|v| v.as_str()) {
me.dark = s.to_owned();
}
if let Some(s) = t.get("light").and_then(|v| v.as_str()) {
me.light = s.to_owned();
}
}
me
}
}
impl Flavor { impl Flavor {
pub(crate) fn from_theme(theme: &Spanned<DeTable>, input: &str) -> Result<Self, toml::de::Error> {
if let Some(value) = theme.get_ref().get("flavor").cloned() {
error_with_input(Self::deserialize(value.into_deserializer()), input)
} else {
Ok(Self::default())
}
}
pub(crate) fn read(&self, light: bool) -> Result<String> { pub(crate) fn read(&self, light: bool) -> Result<String> {
Ok(match if light { self.light.as_str() } else { self.dark.as_str() } { Ok(match if light { self.light.as_str() } else { self.dark.as_str() } {
"" => String::new(), "" => String::new(),

View file

@ -2,13 +2,13 @@ use std::path::PathBuf;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use serde::Deserialize; use serde::Deserialize;
use yazi_codegen::{DeserializeOver1, DeserializeOver2}; use yazi_codegen::{DeserializeOver, DeserializeOver1, DeserializeOver2};
use yazi_fs::{Xdg, ok_or_not_found}; use yazi_fs::{Xdg, ok_or_not_found};
use super::{Filetype, Flavor, Icon}; use super::{Filetype, Flavor, Icon};
use crate::{Style, normalize_path}; use crate::{Style, normalize_path};
#[derive(Deserialize, DeserializeOver1)] #[derive(Deserialize, DeserializeOver, DeserializeOver1)]
pub struct Theme { pub struct Theme {
pub flavor: Flavor, pub flavor: Flavor,
pub app: App, pub app: App,

View file

@ -2,11 +2,13 @@ use std::ops::Deref;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize, de::IntoDeserializer};
use toml::{Spanned, de::DeTable};
use yazi_codegen::DeserializeOver;
use crate::vfs::Service; use crate::vfs::Service;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize, DeserializeOver)]
pub struct Services(HashMap<String, Service>); pub struct Services(HashMap<String, Service>);
impl Deref for Services { impl Deref for Services {
@ -30,12 +32,13 @@ impl Services {
Ok(self) Ok(self)
} }
pub(super) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result<Self, D::Error> pub(super) fn deserialize_over_with<'de>(
where mut self,
D: serde::Deserializer<'de>, table: Spanned<DeTable<'de>>,
{ ) -> Result<Self, toml::de::Error> {
let map: HashMap<String, Service> = HashMap::deserialize(deserializer)?; for (key, value) in table.into_inner() {
self.0.extend(map); self.0.insert(key.into_inner().into_owned(), <_>::deserialize(value.into_deserializer())?);
}
Ok(self) Ok(self)
} }

View file

@ -3,13 +3,13 @@ use std::io;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use yazi_codegen::DeserializeOver1; use yazi_codegen::{DeserializeOver, DeserializeOver1};
use yazi_fs::{Xdg, ok_or_not_found}; use yazi_fs::{Xdg, ok_or_not_found};
use super::Service; use super::Service;
use crate::{Preset, vfs::Services}; use crate::{Preset, vfs::Services};
#[derive(Deserialize, Serialize, DeserializeOver1)] #[derive(Deserialize, Serialize, DeserializeOver, DeserializeOver1)]
pub struct Vfs { pub struct Vfs {
pub services: Services, pub services: Services,
} }
@ -19,11 +19,9 @@ impl Vfs {
pub static LOADED: OnceCell<Vfs> = OnceCell::const_new(); pub static LOADED: OnceCell<Vfs> = OnceCell::const_new();
async fn init() -> io::Result<Vfs> { async fn init() -> io::Result<Vfs> {
tokio::task::spawn_blocking(|| { tokio::task::spawn_blocking(|| Preset::vfs()?.deserialize_over(&Vfs::read()?)?.reshape())
Preset::vfs()?.deserialize_over(toml::Deserializer::parse(&Vfs::read()?)?)?.reshape() .await?
}) .map_err(io::Error::other)
.await?
.map_err(io::Error::other)
} }
LOADED.get_or_try_init(init).await LOADED.get_or_try_init(init).await

View file

@ -1,11 +1,11 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;
use yazi_codegen::DeserializeOver1; use yazi_codegen::{DeserializeOver, DeserializeOver1};
use yazi_fs::{Xdg, ok_or_not_found}; use yazi_fs::{Xdg, ok_or_not_found};
use crate::{mgr, open, opener, plugin, popup, preview, tasks, which}; use crate::{mgr, open, opener, plugin, popup, preview, tasks, which};
#[derive(Deserialize, DeserializeOver1)] #[derive(Deserialize, DeserializeOver, DeserializeOver1)]
pub struct Yazi { pub struct Yazi {
pub mgr: mgr::Mgr, pub mgr: mgr::Mgr,
pub preview: preview::Preview, pub preview: preview::Preview,

View file

@ -79,28 +79,22 @@ function M:tidy(from, to, tmp)
fail("No files extracted from '%s'", from) fail("No files extracted from '%s'", from)
end end
local only = #outs == 1 local only = #outs == 1 and outs[1]
if only and not outs[1].cha.is_dir and require("archive").is_tar(outs[1].url) then if only and not only.cha.is_dir and require("archive").is_tar(only.url) then
self:entry { args = { tostring(outs[1].url), tostring(to) } } self:entry { args = { tostring(only.url), tostring(to) } }
fs.remove("file", outs[1].url) fs.remove("file", only.url)
fs.remove("dir", tmp) fs.remove("dir", tmp)
return return
end end
local target local target = to:join(only and only.name or self.trim_ext(from.name))
if only then target = fs.unique(only and not only.cha.is_dir and "file" or "dir", target)
target = to:join(outs[1].name)
else
target = to:join(self.trim_ext(from.name))
end
target = fs.unique(only and "file" or "dir", target)
if not target then if not target then
fail("Failed to determine a target for '%s'", from) fail("Failed to determine a target for '%s'", from)
end end
if only and not fs.rename(outs[1].url, target) then if only and not fs.rename(only.url, target) then
fail('Failed to move "%s" to "%s"', outs[1].url, target) fail('Failed to move "%s" to "%s"', only.url, target)
elseif not only and not fs.rename(tmp, target) then elseif not only and not fs.rename(tmp, target) then
fail('Failed to move "%s" to "%s"', tmp, target) fail('Failed to move "%s" to "%s"', tmp, target)
end end