Commit 92fbde37d644
tarball2 files changed+78-5
@@ -898,6 +898,8 @@ pub struct Loaded {
898898
/// syntax error costs its own line rather than the whole file. An unclosed899899
/// table or array is reported lines after the mistake, so the culprit is900900
/// the nearby line whose removal clears that error, or else gets furthest.901
+/// A table header is never dropped alone, since its keys would then join the902
+/// table above and change what they mean. It takes its keys with it.901903
fn lenient_table(text: &str, w: &mut Vec<String>) -> Result<toml::Table, Error> {902904
const MAX_BAD_LINES: usize = 32;903905
const LOOK_BACK: usize = 4;@@ -931,14 +933,32 @@ fn lenient_table(text: &str, w: &mut Vec<String>) -> Result<toml::Table, Error>
931933
};932934
let culprit = (at.saturating_sub(LOOK_BACK)..=at)933935
.filter(|&i| !lines[i].1.trim().is_empty())934
- .max_by_key(|&i| progress(i))936
+ .max_by_key(|&i| (progress(i), !is_header(lines[i].1)))935937
.unwrap_or(at);936
- let (line, _) = lines.remove(culprit);937
- w.push(format!("line {line}: {}, left out", e.message().trim()));938
+ let (line, text) = lines[culprit];939
+ if is_header(text) {940
+ let end = (culprit + 1..lines.len())941
+ .find(|&i| is_header(lines[i].1))942
+ .unwrap_or(lines.len());943
+ lines.drain(culprit..end);944
+ let name = text.split('#').next().unwrap_or(text).trim();945
+ w.push(format!(946
+ "line {line}: {}, section {name} left out",947
+ e.message().trim()948
+ ));949
+ } else {950
+ lines.remove(culprit);951
+ w.push(format!("line {line}: {}, left out", e.message().trim()));952
+ }938953
}939954
toml::from_str(&join(&lines)).map_err(Error::Toml)940955
}941956
957
+fn is_header(line: &str) -> bool {958
+ let line = line.split('#').next().unwrap_or_default().trim_start();959
+ line.starts_with('[') && !line.contains(['=', ',', '{'])960
+}961
+942962
fn entry<T: DeserializeOwned>(name: &str, value: toml::Value, w: &mut Vec<String>) -> Option<T> {943963
value944964
.try_into()@@ -1161,6 +1181,58 @@ mod tests {
11611181
}11621182
11631183
#[test]1184
+ fn a_broken_table_header_takes_its_section_with_it() {1185
+ let loaded = Config::parse(1186
+ r##"1187
+ [general]1188
+ focus_follows_mouse = false1189
+ [binds1190
+ "Mod+q" = "close"1191
+ [decoration]1192
+ radius = [1, 2, 3, 4]1193
+ "##,1194
+ )1195
+ .unwrap();1196
+ let c = loaded.config;1197
+ assert!(!c.general.focus_follows_mouse);1198
+ assert_eq!(c.binds.len(), 0);1199
+ assert_eq!(c.decoration.radius, Corners([1, 2, 3, 4]));1200
+ assert_eq!(loaded.warnings.len(), 1, "{:#?}", loaded.warnings);1201
+ assert!(1202
+ loaded.warnings[0].contains("section [binds left out"),1203
+ "{:#?}",1204
+ loaded.warnings1205
+ );1206
+ }1207
+1208
+ #[test]1209
+ fn a_broken_rule_header_does_not_feed_the_rule_above() {1210
+ let loaded = Config::parse(1211
+ r##"1212
+ [[rules]]1213
+ app_id = "^a$"1214
+ floating = true1215
+ [[rules]1216
+ app_id = "^b$"1217
+ fullscreen = true1218
+ [[rules]]1219
+ app_id = "^c$"1220
+ "##,1221
+ )1222
+ .unwrap();1223
+ let apps: Vec<_> = loaded1224
+ .config1225
+ .rules1226
+ .iter()1227
+ .map(|r| format!("{r:?}"))1228
+ .collect();1229
+ assert_eq!(apps.len(), 2, "{:#?} {:#?}", apps, loaded.warnings);1230
+ assert!(apps[0].contains("floating: Some(true)") && !apps[0].contains("fullscreen: Some"));1231
+ assert_eq!(loaded.warnings.len(), 1, "{:#?}", loaded.warnings);1232
+ assert!(loaded.warnings[0].contains("section [[rules] left out"));1233
+ }1234
+1235
+ #[test]11641236
fn a_bad_device_option_costs_only_itself() {11651237
let loaded = Config::parse(11661238
r#"@@ -222,8 +222,9 @@ impl State {
222222
}223223
}224224
225
- /// Swaps in a freshly parsed config. A file that is not TOML leaves the226
- /// running config untouched.225
+ /// Swaps in a freshly parsed config. A file too broken to repair line by226
+ /// line leaves the running config untouched, anything less is loaded with227
+ /// warnings for what had to be left out.227228
pub fn reload(&mut self) -> Result<(), String> {228229
let loaded = Config::load(self.settings.path.as_deref()).map_err(|e| e.to_string())?;229230
let mut warnings = loaded.warnings.clone();