From dfeafa995ccfb8ba54f40fa860ceaf5239bf7252 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Mon, 2 Mar 2026 09:21:01 +0800 Subject: [PATCH] feat: keep span info when config parsing fails --- yazi-codegen/src/lib.rs | 151 +++++++++---------------- yazi-config/src/keymap/keymap.rs | 4 +- yazi-config/src/lib.rs | 38 +++++-- yazi-config/src/opener/opener.rs | 19 ++-- yazi-config/src/theme/flavor.rs | 29 ++--- yazi-config/src/theme/theme.rs | 4 +- yazi-config/src/vfs/services.rs | 19 ++-- yazi-config/src/vfs/vfs.rs | 12 +- yazi-config/src/yazi.rs | 4 +- yazi-plugin/preset/plugins/extract.lua | 14 +-- 10 files changed, 132 insertions(+), 162 deletions(-) diff --git a/yazi-codegen/src/lib.rs b/yazi-codegen/src/lib.rs index d5d40ba0..86dff6a0 100644 --- a/yazi-codegen/src/lib.rs +++ b/yazi-codegen/src/lib.rs @@ -1,40 +1,47 @@ use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{Attribute, Data, DeriveInput, Fields, parse_macro_input}; +use quote::quote; +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 { + crate::error_with_input(self.deserialize_over_with(crate::parse_recoverable(input)?), input) + } + } + } + .into() +} #[proc_macro_derive(DeserializeOver1)] pub fn deserialize_over1(input: TokenStream) -> TokenStream { - // Parse the input tokens into a syntax tree - let input = parse_macro_input!(input as DeriveInput); + let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput); - // Get the name of the struct - let name = &input.ident; - 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 { + let assignments = match data { + Data::Struct(struct_) => match struct_.fields { Fields::Named(fields) => { - let mut shadow_fields = Vec::with_capacity(fields.named.len()); - let mut field_calls = Vec::with_capacity(fields.named.len()); + let mut assignments = Vec::with_capacity(fields.named.len()); - for field in &fields.named { - let name = &field.ident; - let attrs: Vec<&Attribute> = - field.attrs.iter().filter(|&a| a.path().is_ident("serde")).collect(); + for field in fields.named { + let field_ident = &field.ident; + let field_name = field_ident.as_ref().unwrap().to_string(); - shadow_fields.push(quote! { - #(#attrs)* - pub(crate) #name: Option - }); - field_calls.push(quote! { - if let Some(value) = shadow.#name { - self.#name = self.#name.deserialize_over(value).map_err(serde::de::Error::custom)?; + assignments.push(quote! { + if let Some(value) = table.remove(#field_name) { + let span = value.span(); + let table = match value.into_inner() { + toml::de::DeValue::Table(table) => table, + _ => return Err(serde::de::Error::custom(format!("expected top-level `{}` to be a TOML table", #field_name))), + }; + 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"), }, @@ -42,34 +49,11 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream { }; quote! { - #[derive(serde::Deserialize)] - pub(crate) struct #shadow_name { - #(#shadow_fields),* - } - - impl #name { + impl #ident { #[inline] - pub(crate) fn deserialize_over<'de, D>(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - self.deserialize_over_with::(Self::deserialize_shadow(deserializer)?) - } - - #[inline] - pub(crate) fn deserialize_shadow<'de, D>(deserializer: D) -> Result<#shadow_name, D::Error> - where - D: serde::Deserializer<'de>, - { - #shadow_name::deserialize(deserializer) - } - - #[inline] - pub(crate) fn deserialize_over_with<'de, D>(mut self, shadow: #shadow_name) -> Result - where - D: serde::Deserializer<'de>, - { - #(#field_calls)* + pub(crate) fn deserialize_over_with<'de>(mut self, table: toml::Spanned>) -> Result { + let mut table = table.into_inner(); + #(#assignments)* Ok(self) } } @@ -79,33 +63,26 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream { #[proc_macro_derive(DeserializeOver2)] pub fn deserialize_over2(input: TokenStream) -> TokenStream { - // Parse the input tokens into a syntax tree - let input = parse_macro_input!(input as DeriveInput); + let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput); - // Get the name of the struct - let name = &input.ident; - 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 { + let assignments = match data { + Data::Struct(struct_) => match struct_.fields { Fields::Named(fields) => { - let mut shadow_fields = Vec::with_capacity(fields.named.len()); - let mut field_assignments = Vec::with_capacity(fields.named.len()); + let mut assignments = Vec::with_capacity(fields.named.len()); - for field in &fields.named { - let (ty, name) = (&field.ty, &field.ident); - shadow_fields.push(quote! { - pub(crate) #name: Option<#ty> - }); - field_assignments.push(quote! { - if let Some(value) = shadow.#name { - self.#name = value; + for field in fields.named { + let (ty, field_ident) = (field.ty, field.ident); + let field_name = field_ident.as_ref().unwrap().to_string(); + + assignments.push(quote! { + if let Some(value) = table.remove(#field_name) { + let de = serde::de::IntoDeserializer::into_deserializer(value); + self.#field_ident = <#ty as serde::Deserialize>::deserialize(de)?; } }); } - (shadow_fields, field_assignments) + assignments } _ => panic!("DeserializeOver2 only supports structs with named fields"), }, @@ -113,32 +90,12 @@ pub fn deserialize_over2(input: TokenStream) -> TokenStream { }; quote! { - #[derive(serde::Deserialize)] - pub(crate) struct #shadow_name { - #(#shadow_fields),* - } - - impl #name { + impl #ident { #[inline] - pub(crate) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result - where - D: serde::Deserializer<'de> - { - Ok(self.deserialize_over_with(Self::deserialize_shadow(deserializer)?)) - } - - #[inline] - pub(crate) fn deserialize_shadow<'de, D>(deserializer: D) -> Result<#shadow_name, D::Error> - where - D: serde::Deserializer<'de> - { - #shadow_name::deserialize(deserializer) - } - - #[inline] - pub(crate) fn deserialize_over_with(mut self, shadow: #shadow_name) -> Self { - #(#field_assignments)* - self + pub(crate) fn deserialize_over_with<'de>(mut self, table: toml::Spanned>) -> Result { + let mut table = table.into_inner(); + #(#assignments)* + Ok(self) } } } diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index 4d922026..a6e3b082 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -1,12 +1,12 @@ use anyhow::{Context, Result}; use serde::Deserialize; -use yazi_codegen::DeserializeOver1; +use yazi_codegen::{DeserializeOver, DeserializeOver1}; use yazi_fs::{Xdg, ok_or_not_found}; use yazi_shared::Layer; use super::{Chord, KeymapRules}; -#[derive(Deserialize, DeserializeOver1)] +#[derive(Deserialize, DeserializeOver, DeserializeOver1)] pub struct Keymap { pub mgr: KeymapRules, pub tasks: KeymapRules, diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 023e796c..2101d911 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -4,6 +4,7 @@ yazi_macro::mod_flat!(icon layout pattern platform preset priority style utils y use std::io::{Read, Write}; +use toml::{Spanned, de::DeTable}; use yazi_shared::{RoCell, SyncCell}; use yazi_tty::TTY; @@ -25,8 +26,8 @@ fn try_init(merge: bool) -> anyhow::Result<()> { let mut keymap = Preset::keymap()?; if merge { - yazi = yazi.deserialize_over(toml::Deserializer::parse(&yazi::Yazi::read()?)?)?; - keymap = keymap.deserialize_over(toml::Deserializer::parse(&keymap::Keymap::read()?)?)?; + yazi = yazi.deserialize_over(&yazi::Yazi::read()?)?; + keymap = keymap.deserialize_over(&keymap::Keymap::read()?)?; } YAZI.init(yazi.reshape()?); @@ -43,17 +44,19 @@ pub fn init_flavor(light: 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 { - let shadow = - theme::Theme::deserialize_shadow(toml::Deserializer::parse(&theme::Theme::read()?)?)?; - let flavor = shadow.flavor.as_ref().map(theme::Flavor::from).unwrap_or_default().read(light)?; - theme = theme.deserialize_over(toml::Deserializer::parse(&flavor)?)?; - theme = theme.deserialize_over_with::(shadow)?; + let theme_str = theme::Theme::read()?; + let theme = parse_recoverable(&theme_str)?; + + let flavor_str = theme::Flavor::from_theme(&theme, &theme_str)?.read(light)?; + + 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(()) } @@ -78,3 +81,20 @@ fn wait_for_key(e: anyhow::Error) -> anyhow::Result<()> { TTY.reader().read_exact(&mut [0])?; Ok(()) } + +pub(crate) fn parse_recoverable<'a>( + input: &'a str, +) -> Result>, toml::de::Error> { + let (table, errors) = DeTable::parse_recoverable(input); + if let Some(err) = errors.into_iter().next() { Err(err) } else { Ok(table) } +} + +pub(crate) fn error_with_input( + result: Result, + input: &str, +) -> Result { + result.map_err(|mut err| { + err.set_input(Some(input)); + err + }) +} diff --git a/yazi-config/src/opener/opener.rs b/yazi-config/src/opener/opener.rs index 1263741d..f6b5702e 100644 --- a/yazi-config/src/opener/opener.rs +++ b/yazi-config/src/opener/opener.rs @@ -3,12 +3,14 @@ use std::{mem, ops::Deref}; use anyhow::Result; use hashbrown::HashMap; use indexmap::IndexSet; -use serde::Deserialize; +use serde::{Deserialize, de::IntoDeserializer}; +use toml::{Spanned, de::DeTable}; +use yazi_codegen::DeserializeOver; use super::OpenerRule; use crate::check_for; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, DeserializeOver)] pub struct Opener(HashMap>); impl Deref for Opener { @@ -56,12 +58,13 @@ impl Opener { Ok(self) } - pub(crate) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let map: HashMap> = HashMap::deserialize(deserializer)?; - self.0.extend(map); + pub(crate) fn deserialize_over_with<'de>( + mut self, + table: Spanned>, + ) -> Result { + for (key, value) in table.into_inner() { + self.0.insert(key.into_inner().into_owned(), <_>::deserialize(value.into_deserializer())?); + } Ok(self) } diff --git a/yazi-config/src/theme/flavor.rs b/yazi-config/src/theme/flavor.rs index bc81c60d..57bcfd90 100644 --- a/yazi-config/src/theme/flavor.rs +++ b/yazi-config/src/theme/flavor.rs @@ -1,33 +1,28 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use toml::Value; +use serde::{Deserialize, Serialize, de::IntoDeserializer}; +use toml::{Spanned, de::DeTable}; use yazi_codegen::DeserializeOver2; use yazi_fs::Xdg; +use crate::error_with_input; + #[derive(Default, Deserialize, DeserializeOver2, Serialize)] pub struct Flavor { pub dark: 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 { + pub(crate) fn from_theme(theme: &Spanned, input: &str) -> Result { + 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 { Ok(match if light { self.light.as_str() } else { self.dark.as_str() } { "" => String::new(), diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index 516ae991..76adf672 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -2,13 +2,13 @@ use std::path::PathBuf; use anyhow::{Context, Result, anyhow, bail}; use serde::Deserialize; -use yazi_codegen::{DeserializeOver1, DeserializeOver2}; +use yazi_codegen::{DeserializeOver, DeserializeOver1, DeserializeOver2}; use yazi_fs::{Xdg, ok_or_not_found}; use super::{Filetype, Flavor, Icon}; use crate::{Style, normalize_path}; -#[derive(Deserialize, DeserializeOver1)] +#[derive(Deserialize, DeserializeOver, DeserializeOver1)] pub struct Theme { pub flavor: Flavor, pub app: App, diff --git a/yazi-config/src/vfs/services.rs b/yazi-config/src/vfs/services.rs index 0e8814a2..dc011dec 100644 --- a/yazi-config/src/vfs/services.rs +++ b/yazi-config/src/vfs/services.rs @@ -2,11 +2,13 @@ use std::ops::Deref; use anyhow::{Result, bail}; 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; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, DeserializeOver)] pub struct Services(HashMap); impl Deref for Services { @@ -30,12 +32,13 @@ impl Services { Ok(self) } - pub(super) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let map: HashMap = HashMap::deserialize(deserializer)?; - self.0.extend(map); + pub(super) fn deserialize_over_with<'de>( + mut self, + table: Spanned>, + ) -> Result { + for (key, value) in table.into_inner() { + self.0.insert(key.into_inner().into_owned(), <_>::deserialize(value.into_deserializer())?); + } Ok(self) } diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index 7a405252..954a5f34 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -3,13 +3,13 @@ use std::io; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use tokio::sync::OnceCell; -use yazi_codegen::DeserializeOver1; +use yazi_codegen::{DeserializeOver, DeserializeOver1}; use yazi_fs::{Xdg, ok_or_not_found}; use super::Service; use crate::{Preset, vfs::Services}; -#[derive(Deserialize, Serialize, DeserializeOver1)] +#[derive(Deserialize, Serialize, DeserializeOver, DeserializeOver1)] pub struct Vfs { pub services: Services, } @@ -19,11 +19,9 @@ impl Vfs { pub static LOADED: OnceCell = OnceCell::const_new(); async fn init() -> io::Result { - tokio::task::spawn_blocking(|| { - Preset::vfs()?.deserialize_over(toml::Deserializer::parse(&Vfs::read()?)?)?.reshape() - }) - .await? - .map_err(io::Error::other) + tokio::task::spawn_blocking(|| Preset::vfs()?.deserialize_over(&Vfs::read()?)?.reshape()) + .await? + .map_err(io::Error::other) } LOADED.get_or_try_init(init).await diff --git a/yazi-config/src/yazi.rs b/yazi-config/src/yazi.rs index ba346ccf..6fb243a3 100644 --- a/yazi-config/src/yazi.rs +++ b/yazi-config/src/yazi.rs @@ -1,11 +1,11 @@ use anyhow::{Context, Result}; use serde::Deserialize; -use yazi_codegen::DeserializeOver1; +use yazi_codegen::{DeserializeOver, DeserializeOver1}; use yazi_fs::{Xdg, ok_or_not_found}; use crate::{mgr, open, opener, plugin, popup, preview, tasks, which}; -#[derive(Deserialize, DeserializeOver1)] +#[derive(Deserialize, DeserializeOver, DeserializeOver1)] pub struct Yazi { pub mgr: mgr::Mgr, pub preview: preview::Preview, diff --git a/yazi-plugin/preset/plugins/extract.lua b/yazi-plugin/preset/plugins/extract.lua index 33d6ce5b..fc03db2b 100644 --- a/yazi-plugin/preset/plugins/extract.lua +++ b/yazi-plugin/preset/plugins/extract.lua @@ -79,22 +79,16 @@ function M:tidy(from, to, tmp) fail("No files extracted from '%s'", from) end - local only = #outs == 1 - if only and not outs[1].cha.is_dir and require("archive").is_tar(outs[1].url) then + local only = #outs == 1 and outs[1] + if only and not only.cha.is_dir and require("archive").is_tar(outs[1].url) then self:entry { args = { tostring(outs[1].url), tostring(to) } } fs.remove("file", outs[1].url) fs.remove("dir", tmp) return end - local target - if only then - 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) + local target = to:join(only and only.name or self.trim_ext(from.name)) + target = fs.unique(only and not only.cha.is_dir and "file" or "dir", target) if not target then fail("Failed to determine a target for '%s'", from) end