diff --git a/helix-core/src/modeline.rs b/helix-core/src/modeline.rs index 66b02b60c..7497c65c7 100644 --- a/helix-core/src/modeline.rs +++ b/helix-core/src/modeline.rs @@ -4,14 +4,16 @@ use once_cell::sync::Lazy; use crate::indent::IndentStyle; use crate::regex::Regex; +use crate::syntax::ModelineConfig; use crate::{LineEnding, RopeSlice}; // 5 is the vim default const LINES_TO_CHECK: usize = 5; const LENGTH_TO_CHECK: usize = 256; -static MODELINE_REGEX: Lazy = +static VIM_MODELINE_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(\S*\s+)?(vi|[vV]im[<=>]?\d*|ex):\s*(set?\s+)?").unwrap()); +static HELIX_MODELINE_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(\S*\s+)?helix:").unwrap()); #[derive(Default, Debug, Eq, PartialEq)] pub struct Modeline { @@ -65,7 +67,8 @@ impl Modeline { }; c == ' ' || c == '\t' }; - if let Some(pos) = MODELINE_REGEX.find(line) { + + if let Some(pos) = VIM_MODELINE_REGEX.find(line) { for option in line[pos.end()..].split(split_modeline) { let parts: Vec<_> = option.split('=').collect(); match parts[0] { @@ -93,6 +96,27 @@ impl Modeline { } } } + + if let Some(pos) = HELIX_MODELINE_REGEX.find(line) { + let config = &line[pos.end()..]; + match toml::from_str::(config) { + Ok(modeline) => { + if let Some(language) = modeline.language { + self.language = Some(language); + } + if let Some(indent) = modeline.indent { + self.indent_style = Some(IndentStyle::from_str(&indent.unit)); + } + if let Some(line_ending) = modeline.line_ending { + self.line_ending = LineEnding::from_str(&line_ending); + if self.line_ending.is_none() { + log::warn!("could not interpret line ending {line_ending:?}"); + } + } + } + Err(e) => log::warn!("{e}"), + } + } } } @@ -223,6 +247,41 @@ mod test { ..Default::default() }, ), + ( + "# helix: language = 'perl'", + Modeline { + language: Some("perl".to_string()), + ..Default::default() + }, + ), + ( + "# helix: indent = { unit = ' ' }", + Modeline { + indent_style: Some(IndentStyle::Spaces(3)), + ..Default::default() + }, + ), + ( + "# helix: indent = { unit = \"\t\" }", + Modeline { + indent_style: Some(IndentStyle::Tabs), + ..Default::default() + }, + ), + ( + "# helix: indent = { unit = \"\\t\" }", + Modeline { + indent_style: Some(IndentStyle::Tabs), + ..Default::default() + }, + ), + ( + "# helix: line-ending = \"\\r\\n\"", + Modeline { + line_ending: Some(LineEnding::Crlf), + ..Default::default() + }, + ), ]; for (line, expected) in tests { let mut got = Modeline::default(); diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 3cf818f60..19674c440 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -171,6 +171,18 @@ pub struct LanguageConfiguration { pub persistent_diagnostic_sources: Vec, } +/// The subset of LanguageConfig which can be read from a modeline. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct ModelineConfig { + /// the language name (corresponds to language_id in LanguageConfig) + pub language: Option, + /// the indent settings (only unit is supported in modelines) + pub indent: Option, + /// the line ending to use (as a literal string) + pub line_ending: Option, +} + #[derive(Debug, PartialEq, Eq, Hash)] pub enum FileType { /// The extension of the file, either the `Path::extension` or the full @@ -536,6 +548,12 @@ pub struct DebuggerQuirks { pub absolute_paths: bool, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ModelineIndentationConfiguration { + pub unit: String, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct IndentationConfiguration {