From beedd3e33598dbc3ff051db087ab63d290595cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 1 Jun 2026 21:54:29 +0300 Subject: [PATCH] Support ex: in vim modelines (#58121) Ref https://vimdoc.sourceforge.net/htmldoc/options.html#ex%3A Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes N/A Release Notes: - Support `ex:` identifier in vim modelines. --------- Co-authored-by: dino --- crates/language/src/modeline.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/language/src/modeline.rs b/crates/language/src/modeline.rs index 8b7e604449..056b93e716 100644 --- a/crates/language/src/modeline.rs +++ b/crates/language/src/modeline.rs @@ -205,13 +205,16 @@ fn parse_vim_modelines(modelines: &[&str], settings: &mut ModelineSettings) { } } +// Vim requires whitespace before the `{vi:|vim:|ex:}` marker. As an exception, +// `vi:` and `vim:` may also appear at the start of a line, but `ex:` may not as +// Vim ignores a leading `ex:` since it could be short for `example:`. static VIM_MODELINE_PATTERNS: LazyLock> = LazyLock::new(|| { [ - // Second form: [text{white}]{vi:vim:Vim:}[white]se[t] {options}:[text] + // Second form: [text{white}]{vi:|vim:|Vim:|ex:}[white]se[t] {options}:[text] // Allow escaped colons in options: match non-colon chars or backslash followed by any char - r"(?:^|\s)(vi|vim|Vim):(?:\s*)se(?:t)?\s+((?:[^\\:]|\\.)*):", - // First form: [text{white}]{vi:vim:}[white]{options} - r"(?:^|\s+)(vi|vim):(?:\s*(.+))", + r"(?:(?:^|\s)(?:vi|vim|Vim)|\sex):\s*se(?:t)?\s+((?:[^\\:]|\\.)*):", + // First form: [text{white}]{vi:|vim:|ex:}[white]{options} + r"(?:(?:^|\s)(?:vi|vim)|\sex):\s*(.+)", ] .iter() .map(|pattern| Regex::new(pattern).expect("valid regex")) @@ -225,7 +228,7 @@ static VIM_MODELINE_PATTERNS: LazyLock> = LazyLock::new(|| { fn parse_vim_modeline(line: &str, settings: &mut ModelineSettings) { for re in VIM_MODELINE_PATTERNS.iter() { if let Some(captures) = re.captures(line) { - if let Some(options) = captures.get(2) { + if let Some(options) = captures.get(1) { parse_vim_settings(options.as_str().trim(), settings); break; } @@ -465,6 +468,21 @@ mod tests { ..Default::default() } ); + + // Using `ex:` should only be interpreted when whitespace separates it + // from the beginning of the line. + let content = "ex: filetype=shell"; + assert_eq!(parse_modeline(&[content], &[]), None); + + let content = "\tex: filetype=shell"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("shell".to_string()), + ..Default::default() + } + ); } #[test]