Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/editor_config.rs')
| -rw-r--r-- | helix-core/src/editor_config.rs | 334 |
1 files changed, 0 insertions, 334 deletions
diff --git a/helix-core/src/editor_config.rs b/helix-core/src/editor_config.rs deleted file mode 100644 index 2eaeaca4..00000000 --- a/helix-core/src/editor_config.rs +++ /dev/null @@ -1,334 +0,0 @@ -//! Support for [EditorConfig](https://EditorConfig.org) configuration loading. -//! -//! EditorConfig is an editor-agnostic format for specifying configuration in an INI-like, human -//! friendly syntax in `.editorconfig` files (which are intended to be checked into VCS). This -//! module provides functions to search for all `.editorconfig` files that apply to a given path -//! and returns an `EditorConfig` type containing any specified configuration options. -//! -//! At time of writing, this module follows the [spec](https://spec.editorconfig.org/) at -//! version 0.17.2. - -use std::{ - collections::HashMap, - fs, - num::{NonZeroU16, NonZeroU8}, - path::Path, - str::FromStr, -}; - -use encoding_rs::Encoding; -use globset::{GlobBuilder, GlobMatcher}; - -use crate::{ - indent::{IndentStyle, MAX_INDENT}, - LineEnding, -}; - -/// Configuration declared for a path in `.editorconfig` files. -#[derive(Debug, Default, PartialEq, Eq)] -pub struct EditorConfig { - pub indent_style: Option<IndentStyle>, - pub tab_width: Option<NonZeroU8>, - pub line_ending: Option<LineEnding>, - pub encoding: Option<&'static Encoding>, - // pub spelling_language: Option<SpellingLanguage>, - pub trim_trailing_whitespace: Option<bool>, - pub insert_final_newline: Option<bool>, - pub max_line_length: Option<NonZeroU16>, -} - -impl EditorConfig { - /// Finds any configuration in `.editorconfig` files which applies to the given path. - /// - /// If no configuration applies then `EditorConfig::default()` is returned. - pub fn find(path: &Path) -> Self { - let mut configs = Vec::new(); - // <https://spec.editorconfig.org/#file-processing> - for ancestor in path.ancestors() { - let editor_config_file = ancestor.join(".editorconfig"); - let Ok(contents) = fs::read_to_string(&editor_config_file) else { - continue; - }; - let ini = match contents.parse::<Ini>() { - Ok(ini) => ini, - Err(err) => { - log::warn!("Ignoring EditorConfig file at '{editor_config_file:?}' because a glob failed to compile: {err}"); - continue; - } - }; - let is_root = ini.pairs.get("root").map(AsRef::as_ref) == Some("true"); - configs.push((ini, ancestor)); - // > The search shall stop if an EditorConfig file is found with the `root` key set to - // > `true` in the preamble or when reaching the root filesystem directory. - if is_root { - break; - } - } - - let mut pairs = Pairs::new(); - // Reverse the configuration stack so that the `.editorconfig` files closest to `path` - // are applied last and overwrite settings in files closer to the search ceiling. - // - // > If multiple EditorConfig files have matching sections, the pairs from the closer - // > EditorConfig file are read last, so pairs in closer files take precedence. - for (config, dir) in configs.into_iter().rev() { - let relative_path = path.strip_prefix(dir).expect("dir is an ancestor of path"); - - for section in config.sections { - if section.glob.is_match(relative_path) { - log::info!( - "applying EditorConfig from section '{}' in file {:?}", - section.glob.glob(), - dir.join(".editorconfig") - ); - pairs.extend(section.pairs); - } - } - } - - Self::from_pairs(pairs) - } - - fn from_pairs(pairs: Pairs) -> Self { - enum IndentSize { - Tab, - Spaces(NonZeroU8), - } - - // <https://spec.editorconfig.org/#supported-pairs> - let indent_size = pairs.get("indent_size").and_then(|value| { - if value.as_ref() == "tab" { - Some(IndentSize::Tab) - } else if let Ok(spaces) = value.parse::<NonZeroU8>() { - Some(IndentSize::Spaces(spaces)) - } else { - None - } - }); - let tab_width = pairs - .get("tab_width") - .and_then(|value| value.parse::<NonZeroU8>().ok()) - .or(match indent_size { - Some(IndentSize::Spaces(spaces)) => Some(spaces), - _ => None, - }); - let indent_style = pairs - .get("indent_style") - .and_then(|value| match value.as_ref() { - "tab" => Some(IndentStyle::Tabs), - "space" => { - let spaces = match indent_size { - Some(IndentSize::Spaces(spaces)) => spaces.get(), - Some(IndentSize::Tab) => tab_width.map(|n| n.get()).unwrap_or(4), - None => 4, - }; - Some(IndentStyle::Spaces(spaces.clamp(1, MAX_INDENT))) - } - _ => None, - }); - let line_ending = pairs - .get("end_of_line") - .and_then(|value| match value.as_ref() { - "lf" => Some(LineEnding::LF), - "crlf" => Some(LineEnding::Crlf), - #[cfg(feature = "unicode-lines")] - "cr" => Some(LineEnding::CR), - _ => None, - }); - let encoding = pairs.get("charset").and_then(|value| match value.as_ref() { - "latin1" => Some(encoding_rs::WINDOWS_1252), - "utf-8" => Some(encoding_rs::UTF_8), - // `utf-8-bom` is intentionally ignored. - // > `utf-8-bom` is discouraged. - "utf-16le" => Some(encoding_rs::UTF_16LE), - "utf-16be" => Some(encoding_rs::UTF_16BE), - _ => None, - }); - let trim_trailing_whitespace = - pairs - .get("trim_trailing_whitespace") - .and_then(|value| match value.as_ref() { - "true" => Some(true), - "false" => Some(false), - _ => None, - }); - let insert_final_newline = pairs - .get("insert_final_newline") - .and_then(|value| match value.as_ref() { - "true" => Some(true), - "false" => Some(false), - _ => None, - }); - // This option is not in the spec but is supported by some editors. - // <https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#max_line_length> - let max_line_length = pairs - .get("max_line_length") - .and_then(|value| value.parse::<NonZeroU16>().ok()); - - Self { - indent_style, - tab_width, - line_ending, - encoding, - trim_trailing_whitespace, - insert_final_newline, - max_line_length, - } - } -} - -type Pairs = HashMap<Box<str>, Box<str>>; - -#[derive(Debug)] -struct Section { - glob: GlobMatcher, - pairs: Pairs, -} - -#[derive(Debug, Default)] -struct Ini { - pairs: Pairs, - sections: Vec<Section>, -} - -impl FromStr for Ini { - type Err = globset::Error; - - fn from_str(source: &str) -> Result<Self, Self::Err> { - // <https://spec.editorconfig.org/#file-format> - let mut ini = Ini::default(); - // > EditorConfig files are in an INI-like file format. To read an EditorConfig file, take - // > one line at a time, from beginning to end. For each line: - for full_line in source.lines() { - // > 1. Remove all leading and trailing whitespace. - let line = full_line.trim(); - // > 2. Process the remaining text as specified for its type below. - // > The types of lines are: - // > * Blank: contains nothing. Blank lines are ignored. - if line.is_empty() { - continue; - } - // > * Comment: starts with a ';' or '#'. Comment lines are ignored. - if line.starts_with([';', '#']) { - continue; - } - if let Some(section) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { - // > * Section Header: starts with a `[` and ends with a `]`. These lines define - // > globs... - - // <https://spec.editorconfig.org/#glob-expressions> - // We need to modify the glob string slightly since EditorConfig's glob flavor - // doesn't match `globset`'s exactly. `globset` only allows '**' at the beginning - // or end of a glob or between two '/'s. (This replacement is not very fancy but - // should cover most practical cases.) - let mut glob_str = section.replace("**.", "**/*."); - if !is_glob_relative(section) { - glob_str.insert_str(0, "**/"); - } - let glob = GlobBuilder::new(&glob_str) - .literal_separator(true) - .backslash_escape(true) - .empty_alternates(true) - .build()?; - ini.sections.push(Section { - glob: glob.compile_matcher(), - pairs: Pairs::new(), - }); - } else if let Some((key, value)) = line.split_once('=') { - // > * Key-Value Pair (or Pair): contains a key and a value, separated by an `=`. - // > * Key: The part before the first `=` on the line. - // > * Value: The part, if any, after the first `=` on the line. - // > * Keys and values are trimmed of leading and trailing whitespace, but - // > include any whitespace that is between non-whitespace characters. - // > * If a value is not provided, then the value is an empty string. - let key = key.trim().to_lowercase().into_boxed_str(); - let value = value.trim().to_lowercase().into_boxed_str(); - if let Some(section) = ini.sections.last_mut() { - section.pairs.insert(key, value); - } else { - ini.pairs.insert(key, value); - } - } - } - Ok(ini) - } -} - -/// Determines whether a glob is relative to the directory of the config file. -fn is_glob_relative(source: &str) -> bool { - // > If the glob contains a path separator (a `/` not inside square brackets), then the - // > glob is relative to the directory level of the particular `.editorconfig` file itself. - let mut idx = 0; - while let Some(open) = source[idx..].find('[').map(|open| idx + open) { - if source[..open].contains('/') { - return true; - } - idx = source[open..] - .find(']') - .map_or(source.len(), |close| idx + close); - } - source[idx..].contains('/') -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn is_glob_relative_test() { - assert!(is_glob_relative("subdir/*.c")); - assert!(!is_glob_relative("*.txt")); - assert!(!is_glob_relative("[a/b].c")); - } - - fn editor_config(path: impl AsRef<Path>, source: &str) -> EditorConfig { - let path = path.as_ref(); - let ini = source.parse::<Ini>().unwrap(); - let pairs = ini - .sections - .into_iter() - .filter(|section| section.glob.is_match(path)) - .fold(Pairs::new(), |mut acc, section| { - acc.extend(section.pairs); - acc - }); - EditorConfig::from_pairs(pairs) - } - - #[test] - fn parse_test() { - let source = r#" - [*] - indent_style = space - - [Makefile] - indent_style = tab - - [docs/**.txt] - insert_final_newline = true - "#; - - assert_eq!( - editor_config("a.txt", source), - EditorConfig { - indent_style: Some(IndentStyle::Spaces(4)), - ..Default::default() - } - ); - assert_eq!( - editor_config("pkg/Makefile", source), - EditorConfig { - indent_style: Some(IndentStyle::Tabs), - ..Default::default() - } - ); - assert_eq!( - editor_config("docs/config/editor.txt", source), - EditorConfig { - indent_style: Some(IndentStyle::Spaces(4)), - insert_final_newline: Some(true), - ..Default::default() - } - ); - } -} |