use std::collections::HashMap;
use std::path::Path;
use std::sync::Mutex;
use ec4rs::property::{EndOfLine, IndentSize, IndentStyle as EcIndentStyle};
use ec4rs::Properties;
use super::{IndentConfig, IndentStyle, LineEnding, TrailingWhitespace};
static CACHE: Mutex<Option<HashMap<String, EditorConfigSettings>>> = Mutex::new(None);
#[derive(Debug, Clone, Default)]
pub struct EditorConfigSettings {
pub line_ending: Option<LineEnding>,
pub indent_style: Option<IndentStyle>,
pub indent_size: Option<usize>,
pub charset: Option<String>,
pub trim_trailing_whitespace: Option<TrailingWhitespace>,
pub insert_final_newline: Option<bool>,
}
impl EditorConfigSettings {
pub fn for_file(path: &Path) -> Self {
if let Some(cached) = Self::get_cached(path) {
return cached;
}
let settings = Self::resolve(path);
Self::cache(path, settings.clone());
settings
}
fn resolve(path: &Path) -> Self {
let properties = match ec4rs::properties_of(path) {
Ok(props) => props,
Err(_) => return Self::default(),
};
Self::from_properties(&properties)
}
fn from_properties(props: &Properties) -> Self {
let mut settings = Self::default();
if let Ok(eol) = props.get::<EndOfLine>() {
settings.line_ending = Some(match eol {
EndOfLine::Lf => LineEnding::Lf,
EndOfLine::CrLf => LineEnding::Crlf,
EndOfLine::Cr => LineEnding::Lf, });
}
if let Ok(style) = props.get::<EcIndentStyle>() {
settings.indent_style = Some(match style {
EcIndentStyle::Tabs => IndentStyle::Tabs,
EcIndentStyle::Spaces => IndentStyle::Spaces,
});
}
if let Ok(size) = props.get::<IndentSize>() {
settings.indent_size = match size {
IndentSize::Value(n) => Some(n),
IndentSize::UseTabWidth => {
props
.get::<ec4rs::property::TabWidth>()
.ok()
.and_then(|tw| match tw {
ec4rs::property::TabWidth::Value(n) => Some(n),
})
.or(Some(4))
}
};
}
if let Ok(charset) = props.get::<ec4rs::property::Charset>() {
settings.charset = Some(match charset {
ec4rs::property::Charset::Latin1 => "latin1".to_string(),
ec4rs::property::Charset::Utf8 => "utf-8".to_string(),
ec4rs::property::Charset::Utf8Bom => "utf-8-bom".to_string(),
ec4rs::property::Charset::Utf16Be => "utf-16be".to_string(),
ec4rs::property::Charset::Utf16Le => "utf-16le".to_string(),
});
}
if let Ok(ec4rs::property::TrimTrailingWs::Value(v)) =
props.get::<ec4rs::property::TrimTrailingWs>()
{
settings.trim_trailing_whitespace = Some(if v {
TrailingWhitespace::Remove
} else {
TrailingWhitespace::Keep
});
}
if let Ok(ec4rs::property::FinalNewline::Value(v)) =
props.get::<ec4rs::property::FinalNewline>()
{
settings.insert_final_newline = Some(v);
}
settings
}
fn get_cached(path: &Path) -> Option<Self> {
let key = Self::cache_key(path)?;
let cache = CACHE.lock().ok()?;
cache.as_ref()?.get(&key).cloned()
}
fn cache(path: &Path, settings: Self) {
if let Some(key) = Self::cache_key(path) {
if let Ok(mut cache) = CACHE.lock() {
let map = cache.get_or_insert_with(HashMap::new);
map.insert(key, settings);
}
}
}
fn cache_key(path: &Path) -> Option<String> {
path.canonicalize()
.ok()
.map(|p| p.to_string_lossy().to_string())
}
#[allow(dead_code)]
pub fn clear_cache() {
if let Ok(mut cache) = CACHE.lock() {
*cache = None;
}
}
pub fn to_indent_config(&self) -> Option<IndentConfig> {
match (self.indent_style, self.indent_size) {
(Some(style), Some(width)) => Some(IndentConfig { style, width }),
(Some(style), None) => Some(IndentConfig {
style,
width: if style == IndentStyle::Tabs { 4 } else { 2 },
}),
(None, Some(width)) => Some(IndentConfig {
style: IndentStyle::Spaces,
width,
}),
(None, None) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_editorconfig(dir: &Path, content: &str) {
fs::write(dir.join(".editorconfig"), content).unwrap();
}
#[test]
fn test_line_ending_mapping() {
let temp = TempDir::new().unwrap();
create_editorconfig(
temp.path(),
r#"
root = true
[*]
end_of_line = lf
"#,
);
let test_file = temp.path().join("test.txt");
fs::write(&test_file, "").unwrap();
EditorConfigSettings::clear_cache();
let settings = EditorConfigSettings::for_file(&test_file);
assert_eq!(settings.line_ending, Some(LineEnding::Lf));
}
#[test]
fn test_crlf_line_ending() {
let temp = TempDir::new().unwrap();
create_editorconfig(
temp.path(),
r#"
root = true
[*]
end_of_line = crlf
"#,
);
let test_file = temp.path().join("test.txt");
fs::write(&test_file, "").unwrap();
EditorConfigSettings::clear_cache();
let settings = EditorConfigSettings::for_file(&test_file);
assert_eq!(settings.line_ending, Some(LineEnding::Crlf));
}
#[test]
fn test_indent_style_tabs() {
let temp = TempDir::new().unwrap();
create_editorconfig(
temp.path(),
r#"
root = true
[*]
indent_style = tab
"#,
);
let test_file = temp.path().join("test.txt");
fs::write(&test_file, "").unwrap();
EditorConfigSettings::clear_cache();
let settings = EditorConfigSettings::for_file(&test_file);
assert_eq!(settings.indent_style, Some(IndentStyle::Tabs));
}
#[test]
fn test_indent_style_spaces() {
let temp = TempDir::new().unwrap();
create_editorconfig(
temp.path(),
r#"
root = true
[*]
indent_style = space
indent_size = 4
"#,
);
let test_file = temp.path().join("test.txt");
fs::write(&test_file, "").unwrap();
EditorConfigSettings::clear_cache();
let settings = EditorConfigSettings::for_file(&test_file);
assert_eq!(settings.indent_style, Some(IndentStyle::Spaces));
assert_eq!(settings.indent_size, Some(4));
}
#[test]
fn test_charset_utf8() {
let temp = TempDir::new().unwrap();
create_editorconfig(
temp.path(),
r#"
root = true
[*]
charset = utf-8
"#,
);
let test_file = temp.path().join("test.txt");
fs::write(&test_file, "").unwrap();
EditorConfigSettings::clear_cache();
let settings = EditorConfigSettings::for_file(&test_file);
assert_eq!(settings.charset, Some("utf-8".to_string()));
}
#[test]
fn test_trim_trailing_whitespace() {
let temp = TempDir::new().unwrap();
create_editorconfig(
temp.path(),
r#"
root = true
[*]
trim_trailing_whitespace = true
"#,
);
let test_file = temp.path().join("test.txt");
fs::write(&test_file, "").unwrap();
EditorConfigSettings::clear_cache();
let settings = EditorConfigSettings::for_file(&test_file);
assert_eq!(
settings.trim_trailing_whitespace,
Some(TrailingWhitespace::Remove)
);
}
#[test]
fn test_insert_final_newline() {
let temp = TempDir::new().unwrap();
create_editorconfig(
temp.path(),
r#"
root = true
[*]
insert_final_newline = true
"#,
);
let test_file = temp.path().join("test.txt");
fs::write(&test_file, "").unwrap();
EditorConfigSettings::clear_cache();
let settings = EditorConfigSettings::for_file(&test_file);
assert_eq!(settings.insert_final_newline, Some(true));
}
#[test]
fn test_no_editorconfig() {
let temp = TempDir::new().unwrap();
let test_file = temp.path().join("test.txt");
fs::write(&test_file, "").unwrap();
EditorConfigSettings::clear_cache();
let settings = EditorConfigSettings::for_file(&test_file);
assert_eq!(settings.line_ending, None);
assert_eq!(settings.indent_style, None);
}
#[test]
fn test_to_indent_config() {
let settings = EditorConfigSettings {
indent_style: Some(IndentStyle::Spaces),
indent_size: Some(4),
..Default::default()
};
let config = settings.to_indent_config().unwrap();
assert_eq!(config.style, IndentStyle::Spaces);
assert_eq!(config.width, 4);
}
#[test]
fn test_pattern_matching() {
let temp = TempDir::new().unwrap();
create_editorconfig(
temp.path(),
r#"
root = true
[*.py]
indent_size = 4
[*.js]
indent_size = 2
"#,
);
let py_file = temp.path().join("test.py");
let js_file = temp.path().join("test.js");
fs::write(&py_file, "").unwrap();
fs::write(&js_file, "").unwrap();
EditorConfigSettings::clear_cache();
let py_settings = EditorConfigSettings::for_file(&py_file);
assert_eq!(py_settings.indent_size, Some(4));
let js_settings = EditorConfigSettings::for_file(&js_file);
assert_eq!(js_settings.indent_size, Some(2));
}
}