use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use lsp_types::Uri;
use crate::config::{Config, RawConfig};
use crate::formatter::FormatStyle;
use super::lint::ServerRules;
use super::uri;
pub(crate) struct ResolvedConfig {
pub(crate) style: FormatStyle,
pub(crate) rules: Arc<ServerRules>,
}
impl ResolvedConfig {
fn resolve(config: &Config, warnings: &mut Vec<String>) -> Arc<Self> {
let (rules, unknown) = ServerRules::from_config(&config.lint);
warnings.extend(
unknown
.into_iter()
.map(|id| format!("unknown rule `{id}` in select/ignore/severity")),
);
Arc::new(Self {
style: FormatStyle::from(&config.format),
rules: Arc::new(rules),
})
}
}
pub(crate) struct ConfigStore {
client: Arc<ResolvedConfig>,
by_dir: HashMap<PathBuf, Arc<ResolvedConfig>>,
}
impl ConfigStore {
pub(crate) fn new(initialization_options: Option<serde_json::Value>) -> (Self, Vec<String>) {
let mut store = Self {
client: ResolvedConfig::resolve(&Config::default(), &mut Vec::new()),
by_dir: HashMap::new(),
};
let warnings = match initialization_options {
Some(options) => store.set_client_settings(options),
None => Vec::new(),
};
(store, warnings)
}
pub(crate) fn set_client_settings(&mut self, settings: serde_json::Value) -> Vec<String> {
self.by_dir.clear();
if settings.is_null() {
return Vec::new();
}
let payload = match settings.get("fatou") {
Some(section) => section.clone(),
None => settings,
};
match serde_json::from_value::<RawConfig>(payload) {
Ok(raw) => {
let (config, mut warnings) = raw.into_config();
self.client = ResolvedConfig::resolve(&config, &mut warnings);
warnings
}
Err(err) => vec![format!("invalid client settings (kept previous): {err}")],
}
}
pub(crate) fn invalidate_discovered(&mut self) {
self.by_dir.clear();
}
pub(crate) fn for_uri(&mut self, uri: &Uri) -> (Arc<ResolvedConfig>, Vec<String>) {
let Some(dir) = uri::to_path(uri).and_then(|path| path.parent().map(PathBuf::from)) else {
return (Arc::clone(&self.client), Vec::new());
};
if let Some(cached) = self.by_dir.get(&dir) {
return (Arc::clone(cached), Vec::new());
}
let mut warnings = Vec::new();
let resolved = match Config::resolve(None, false, &dir) {
Ok((config, Some(source), deprecations)) => {
warnings.extend(
deprecations
.into_iter()
.map(|warning| format!("{}: {warning}", source.display())),
);
ResolvedConfig::resolve(&config, &mut warnings)
}
Ok((_, None, _)) => Arc::clone(&self.client),
Err(err) => {
warnings.push(err.to_string());
Arc::clone(&self.client)
}
};
self.by_dir.insert(dir, Arc::clone(&resolved));
(resolved, warnings)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn uri_for(path: &std::path::Path) -> Uri {
uri::from_path(path).unwrap()
}
fn store_with(settings: serde_json::Value) -> (ConfigStore, Vec<String>) {
ConfigStore::new(Some(settings))
}
#[test]
fn defaults_without_options_or_file() {
let (mut store, warnings) = ConfigStore::new(None);
assert_eq!(warnings, Vec::<String>::new());
let dir = tempfile::tempdir().unwrap();
let (config, warnings) = store.for_uri(&uri_for(&dir.path().join("a.jl")));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(config.style, FormatStyle::default());
}
#[test]
fn client_settings_apply_bare_and_wrapped() {
let bare = serde_json::json!({"format": {"indent-width": 2}});
let (mut store, warnings) = store_with(bare);
assert_eq!(warnings, Vec::<String>::new());
let dir = tempfile::tempdir().unwrap();
let (config, _) = store.for_uri(&uri_for(&dir.path().join("a.jl")));
assert_eq!(config.style.indent_width, 2);
let wrapped = serde_json::json!({"fatou": {"format": {"indent-width": 3}}});
let (mut store, warnings) = store_with(wrapped);
assert_eq!(warnings, Vec::<String>::new());
let (config, _) = store.for_uri(&uri_for(&dir.path().join("a.jl")));
assert_eq!(config.style.indent_width, 3);
}
#[test]
fn discovered_fatou_toml_shadows_client_settings() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("fatou.toml"),
"[format]\nindent-width = 2\n",
)
.unwrap();
let (mut store, _) = store_with(serde_json::json!({"format": {"indent-width": 8}}));
let (config, warnings) = store.for_uri(&uri_for(&dir.path().join("a.jl")));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(config.style.indent_width, 2);
}
#[test]
fn discovery_walks_up_from_a_nested_directory() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("fatou.toml"), "[format]\nline-width = 60\n").unwrap();
let nested = dir.path().join("src").join("inner");
std::fs::create_dir_all(&nested).unwrap();
let (mut store, _) = ConfigStore::new(None);
let (config, _) = store.for_uri(&uri_for(&nested.join("a.jl")));
assert_eq!(config.style.line_width, 60);
}
#[test]
fn invalidation_drops_the_discovery_cache() {
let dir = tempfile::tempdir().unwrap();
let toml = dir.path().join("fatou.toml");
std::fs::write(&toml, "[format]\nindent-width = 2\n").unwrap();
let (mut store, _) = ConfigStore::new(None);
let uri = uri_for(&dir.path().join("a.jl"));
assert_eq!(store.for_uri(&uri).0.style.indent_width, 2);
std::fs::write(&toml, "[format]\nindent-width = 7\n").unwrap();
assert_eq!(store.for_uri(&uri).0.style.indent_width, 2);
store.invalidate_discovered();
assert_eq!(store.for_uri(&uri).0.style.indent_width, 7);
}
#[test]
fn new_client_settings_replace_the_old_and_null_keeps_them() {
let dir = tempfile::tempdir().unwrap();
let uri = uri_for(&dir.path().join("a.jl"));
let (mut store, _) = store_with(serde_json::json!({"format": {"indent-width": 2}}));
assert_eq!(store.for_uri(&uri).0.style.indent_width, 2);
let warnings =
store.set_client_settings(serde_json::json!({"format": {"indent-width": 6}}));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(store.for_uri(&uri).0.style.indent_width, 6);
let warnings = store.set_client_settings(serde_json::Value::Null);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(store.for_uri(&uri).0.style.indent_width, 6);
}
#[test]
fn invalid_client_settings_warn_and_keep_previous() {
let dir = tempfile::tempdir().unwrap();
let uri = uri_for(&dir.path().join("a.jl"));
let (mut store, _) = store_with(serde_json::json!({"format": {"indent-width": 2}}));
let warnings = store.set_client_settings(serde_json::json!({"formt": {}}));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("invalid client settings"));
assert_eq!(store.for_uri(&uri).0.style.indent_width, 2);
}
#[test]
fn broken_fatou_toml_warns_and_falls_back_to_client_settings() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("fatou.toml"), "not toml [").unwrap();
let (mut store, _) = store_with(serde_json::json!({"format": {"indent-width": 8}}));
let uri = uri_for(&dir.path().join("a.jl"));
let (config, warnings) = store.for_uri(&uri);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("failed to parse"));
assert_eq!(config.style.indent_width, 8);
let (_, warnings) = store.for_uri(&uri);
assert_eq!(warnings, Vec::<String>::new());
}
#[test]
fn unknown_rules_and_deprecated_keys_warn_once_per_load() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("fatou.toml"),
"[format]\nline_width = 80\n\n[lint]\nignore = [\"no-such-rule\"]\n",
)
.unwrap();
let (mut store, _) = ConfigStore::new(None);
let uri = uri_for(&dir.path().join("a.jl"));
let (config, warnings) = store.for_uri(&uri);
assert_eq!(config.style.line_width, 80);
assert!(
warnings.iter().any(|w| w.contains("line_width")),
"{warnings:?}"
);
assert!(
warnings.iter().any(|w| w.contains("no-such-rule")),
"{warnings:?}"
);
assert_eq!(store.for_uri(&uri).1, Vec::<String>::new());
}
#[test]
fn a_non_file_uri_gets_the_client_settings() {
let (mut store, _) = store_with(serde_json::json!({"format": {"indent-width": 2}}));
let untitled = Uri::from_str("untitled:Untitled-1").unwrap();
let (config, warnings) = store.for_uri(&untitled);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(config.style.indent_width, 2);
}
}