Skip to repository content782 lines · 27.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:50:53.919Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
modeline.rs
1use regex::Regex;
2use std::{num::NonZeroU32, sync::LazyLock};
3
4/// The settings extracted from an emacs/vim modelines.
5///
6/// The parsing tries to best match the modeline directives and
7/// variables to Zed, matching LanguageSettings fields.
8/// The mode mapping is done later thanks to the LanguageRegistry.
9///
10/// It is not exhaustive, but covers the most common settings.
11#[derive(Debug, Clone, Default, PartialEq)]
12pub struct ModelineSettings {
13 /// The emacs mode or vim filetype.
14 pub mode: Option<String>,
15 /// How many columns a tab should occupy.
16 pub tab_size: Option<NonZeroU32>,
17 /// Whether to indent lines using tab characters, as opposed to multiple
18 /// spaces.
19 pub hard_tabs: Option<bool>,
20 /// The number of bytes that comprise the indentation.
21 pub indent_size: Option<NonZeroU32>,
22 /// Whether to auto-indent lines.
23 pub auto_indent: Option<bool>,
24 /// The column at which to soft-wrap lines.
25 pub preferred_line_length: Option<NonZeroU32>,
26 /// Whether to ensure a final newline at the end of the file.
27 pub ensure_final_newline: Option<bool>,
28 /// Whether to show trailing whitespace on the editor.
29 pub show_trailing_whitespace: Option<bool>,
30
31 /// Emacs modeline variables that were parsed but not mapped to Zed settings.
32 /// Stored as (variable-name, value) pairs.
33 pub emacs_extra_variables: Vec<(String, String)>,
34 /// Vim modeline options that were parsed but not mapped to Zed settings.
35 /// Stored as (option-name, value) pairs.
36 pub vim_extra_variables: Vec<(String, Option<String>)>,
37}
38
39impl ModelineSettings {
40 fn has_settings(&self) -> bool {
41 self != &Self::default()
42 }
43}
44
45/// Parse modelines from file content.
46///
47/// Supports:
48/// - Emacs modelines: -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*- and "Local Variables"
49/// - Vim modelines: vim: set ft=rust ts=4 sw=4 et:
50pub fn parse_modeline(first_lines: &[&str], last_lines: &[&str]) -> Option<ModelineSettings> {
51 let mut settings = ModelineSettings::default();
52
53 parse_modelines(first_lines, &mut settings);
54
55 // Parse Emacs Local Variables in last lines
56 parse_emacs_local_variables(last_lines, &mut settings);
57
58 // Also check for vim modelines in last lines if we don't have settings yet
59 if !settings.has_settings() {
60 parse_vim_modelines(last_lines, &mut settings);
61 }
62
63 Some(settings).filter(|s| s.has_settings())
64}
65
66fn parse_modelines(modelines: &[&str], settings: &mut ModelineSettings) {
67 for line in modelines {
68 parse_emacs_modeline(line, settings);
69 // if emacs is set, do not check for vim modelines
70 if settings.has_settings() {
71 return;
72 }
73 }
74
75 parse_vim_modelines(modelines, settings);
76}
77
78static EMACS_MODELINE_RE: LazyLock<Regex> =
79 LazyLock::new(|| Regex::new(r"-\*-\s*(.+?)\s*-\*-").expect("valid regex"));
80
81/// Parse Emacs-style modelines
82/// Format: -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*-
83/// See Emacs (set-auto-mode)
84fn parse_emacs_modeline(line: &str, settings: &mut ModelineSettings) {
85 let Some(captures) = EMACS_MODELINE_RE.captures(line) else {
86 return;
87 };
88 let Some(modeline_content) = captures.get(1).map(|m| m.as_str()) else {
89 return;
90 };
91 for part in modeline_content.split(';') {
92 parse_emacs_key_value(part, settings, true);
93 }
94}
95
96/// Parse Emacs-style Local Variables block
97///
98/// Emacs supports a "Local Variables" block at the end of files:
99/// ```text
100/// /* Local Variables: */
101/// /* mode: c */
102/// /* tab-width: 4 */
103/// /* End: */
104/// ```
105///
106/// Emacs related code is hack-local-variables--find-variables in
107/// https://cgit.git.savannah.gnu.org/cgit/emacs.git/tree/lisp/files.el#n4346
108fn parse_emacs_local_variables(lines: &[&str], settings: &mut ModelineSettings) {
109 const LOCAL_VARIABLES: &str = "Local Variables:";
110
111 let Some((start_idx, prefix, suffix)) = lines.iter().enumerate().find_map(|(i, line)| {
112 let prefix_len = line.find(LOCAL_VARIABLES)?;
113 let suffix_start = prefix_len + LOCAL_VARIABLES.len();
114 Some((i, line.get(..prefix_len)?, line.get(suffix_start..)?))
115 }) else {
116 return;
117 };
118
119 let mut continuation = String::new();
120
121 for line in &lines[start_idx + 1..] {
122 let Some(content) = line
123 .strip_prefix(prefix)
124 .and_then(|l| l.strip_suffix(suffix))
125 .map(str::trim)
126 else {
127 return;
128 };
129
130 if let Some(continued) = content.strip_suffix('\\') {
131 continuation.push_str(continued);
132 continue;
133 }
134
135 let to_parse = if continuation.is_empty() {
136 content
137 } else {
138 continuation.push_str(content);
139 &continuation
140 };
141
142 if to_parse == "End:" {
143 return;
144 }
145
146 parse_emacs_key_value(to_parse, settings, false);
147 continuation.clear();
148 }
149}
150
151fn parse_emacs_key_value(part: &str, settings: &mut ModelineSettings, bare: bool) {
152 let part = part.trim();
153 if part.is_empty() {
154 return;
155 }
156
157 if let Some((key, value)) = part.split_once(':') {
158 let key = key.trim();
159 let value = value.trim();
160
161 match key.to_lowercase().as_str() {
162 "mode" => {
163 settings.mode = Some(value.to_string());
164 }
165 "c-basic-offset" | "python-indent-offset" => {
166 if let Ok(size) = value.parse::<NonZeroU32>() {
167 settings.indent_size = Some(size);
168 }
169 }
170 "fill-column" => {
171 if let Ok(size) = value.parse::<NonZeroU32>() {
172 settings.preferred_line_length = Some(size);
173 }
174 }
175 "tab-width" => {
176 if let Ok(size) = value.parse::<NonZeroU32>() {
177 settings.tab_size = Some(size);
178 }
179 }
180 "indent-tabs-mode" => {
181 settings.hard_tabs = Some(value != "nil");
182 }
183 "electric-indent-mode" => {
184 settings.auto_indent = Some(value != "nil");
185 }
186 "require-final-newline" => {
187 settings.ensure_final_newline = Some(value != "nil");
188 }
189 "show-trailing-whitespace" => {
190 settings.show_trailing_whitespace = Some(value != "nil");
191 }
192 key => settings
193 .emacs_extra_variables
194 .push((key.to_string(), value.to_string())),
195 }
196 } else if bare {
197 // Handle bare mode specification (e.g., -*- rust -*-)
198 settings.mode = Some(part.to_string());
199 }
200}
201
202fn parse_vim_modelines(modelines: &[&str], settings: &mut ModelineSettings) {
203 for line in modelines {
204 parse_vim_modeline(line, settings);
205 }
206}
207
208// Vim requires whitespace before the `{vi:|vim:|ex:}` marker. As an exception,
209// `vi:` and `vim:` may also appear at the start of a line, but `ex:` may not as
210// Vim ignores a leading `ex:` since it could be short for `example:`.
211static VIM_MODELINE_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
212 [
213 // Second form: [text{white}]{vi:|vim:|Vim:|ex:}[white]se[t] {options}:[text]
214 // Allow escaped colons in options: match non-colon chars or backslash followed by any char
215 r"(?:(?:^|\s)(?:vi|vim|Vim)|\sex):\s*se(?:t)?\s+((?:[^\\:]|\\.)*):",
216 // First form: [text{white}]{vi:|vim:|ex:}[white]{options}
217 r"(?:(?:^|\s)(?:vi|vim)|\sex):\s*(.+)",
218 ]
219 .iter()
220 .map(|pattern| Regex::new(pattern).expect("valid regex"))
221 .collect()
222});
223
224/// Parse Vim-style modelines
225/// Supports both forms:
226/// 1. First form: vi:noai:sw=3 ts=6
227/// 2. Second form: vim: set ft=rust ts=4 sw=4 et:
228fn parse_vim_modeline(line: &str, settings: &mut ModelineSettings) {
229 for re in VIM_MODELINE_PATTERNS.iter() {
230 if let Some(captures) = re.captures(line) {
231 if let Some(options) = captures.get(1) {
232 parse_vim_settings(options.as_str().trim(), settings);
233 break;
234 }
235 }
236 }
237}
238
239fn parse_vim_settings(content: &str, settings: &mut ModelineSettings) {
240 fn split_colon_unescape(input: &str) -> Vec<String> {
241 let mut split = Vec::new();
242 let mut str = String::new();
243 let mut chars = input.chars().peekable();
244 while let Some(c) = chars.next() {
245 if c == '\\' {
246 match chars.next() {
247 Some(escaped_char) => str.push(escaped_char),
248 None => str.push('\\'),
249 }
250 } else if c == ':' {
251 split.push(std::mem::take(&mut str));
252 } else {
253 str.push(c);
254 }
255 }
256 split.push(str);
257 split
258 }
259
260 let parts = split_colon_unescape(content);
261 for colon_part in parts {
262 let colon_part = colon_part.trim();
263 if colon_part.is_empty() {
264 continue;
265 }
266
267 // Each colon part might contain space-separated options
268 for part in colon_part.split_whitespace() {
269 if let Some((key, value)) = part.split_once('=') {
270 match key {
271 "ft" | "filetype" => {
272 settings.mode = Some(value.to_string());
273 }
274 "ts" | "tabstop" => {
275 if let Ok(size) = value.parse::<NonZeroU32>() {
276 settings.tab_size = Some(size);
277 }
278 }
279 "sw" | "shiftwidth" => {
280 if let Ok(size) = value.parse::<NonZeroU32>() {
281 settings.indent_size = Some(size);
282 }
283 }
284 "tw" | "textwidth" => {
285 if let Ok(size) = value.parse::<NonZeroU32>() {
286 settings.preferred_line_length = Some(size);
287 }
288 }
289 _ => {
290 settings
291 .vim_extra_variables
292 .push((key.to_string(), Some(value.to_string())));
293 }
294 }
295 } else {
296 match part {
297 "ai" | "autoindent" => {
298 settings.auto_indent = Some(true);
299 }
300 "noai" | "noautoindent" => {
301 settings.auto_indent = Some(false);
302 }
303 "et" | "expandtab" => {
304 settings.hard_tabs = Some(false);
305 }
306 "noet" | "noexpandtab" => {
307 settings.hard_tabs = Some(true);
308 }
309 "eol" | "endofline" => {
310 settings.ensure_final_newline = Some(true);
311 }
312 "noeol" | "noendofline" => {
313 settings.ensure_final_newline = Some(false);
314 }
315 "set" => {
316 // Ignore the "set" keyword itself
317 }
318 _ => {
319 settings.vim_extra_variables.push((part.to_string(), None));
320 }
321 }
322 }
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use indoc::indoc;
331 use pretty_assertions::assert_eq;
332
333 #[test]
334 fn test_no_modeline() {
335 let content = "This is just regular content\nwith no modeline";
336 assert!(parse_modeline(&[content], &[content]).is_none());
337 }
338
339 #[test]
340 fn test_emacs_bare_mode() {
341 let content = "/* -*- rust -*- */";
342 let settings = parse_modeline(&[content], &[]).unwrap();
343 assert_eq!(
344 settings,
345 ModelineSettings {
346 mode: Some("rust".to_string()),
347 ..Default::default()
348 }
349 );
350 }
351
352 #[test]
353 fn test_emacs_modeline_parsing() {
354 let content = "/* -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*- */";
355 let settings = parse_modeline(&[content], &[]).unwrap();
356 assert_eq!(
357 settings,
358 ModelineSettings {
359 mode: Some("rust".to_string()),
360 tab_size: Some(NonZeroU32::new(4).unwrap()),
361 hard_tabs: Some(false),
362 ..Default::default()
363 }
364 );
365 }
366
367 #[test]
368 fn test_emacs_last_line_parsing() {
369 let content = indoc! {r#"
370 # Local Variables:
371 # compile-command: "cc foo.c -Dfoo=bar -Dhack=whatever \
372 # -Dmumble=blaah"
373 # End:
374 "#}
375 .lines()
376 .collect::<Vec<_>>();
377 let settings = parse_modeline(&[], &content).unwrap();
378 assert_eq!(
379 settings,
380 ModelineSettings {
381 emacs_extra_variables: vec![(
382 "compile-command".to_string(),
383 "\"cc foo.c -Dfoo=bar -Dhack=whatever -Dmumble=blaah\"".to_string()
384 ),],
385 ..Default::default()
386 }
387 );
388
389 let content = indoc! {"
390 foo
391 /* Local Variables: */
392 /* eval: (font-lock-mode -1) */
393 /* mode: old-c */
394 /* mode: c */
395 /* End: */
396 /* mode: ignored */
397 "}
398 .lines()
399 .collect::<Vec<_>>();
400 let settings = parse_modeline(&[], &content).unwrap();
401 assert_eq!(
402 settings,
403 ModelineSettings {
404 mode: Some("c".to_string()),
405 emacs_extra_variables: vec![(
406 "eval".to_string(),
407 "(font-lock-mode -1)".to_string()
408 ),],
409 ..Default::default()
410 }
411 );
412 }
413
414 #[test]
415 fn test_vim_modeline_parsing() {
416 // Test second form (set format)
417 let content = "// vim: set ft=rust ts=4 sw=4 et:";
418 let settings = parse_modeline(&[content], &[]).unwrap();
419 assert_eq!(
420 settings,
421 ModelineSettings {
422 mode: Some("rust".to_string()),
423 tab_size: Some(NonZeroU32::new(4).unwrap()),
424 hard_tabs: Some(false),
425 indent_size: Some(NonZeroU32::new(4).unwrap()),
426 ..Default::default()
427 }
428 );
429
430 // Test first form (colon-separated)
431 let content = "vi:noai:sw=3:ts=6";
432 let settings = parse_modeline(&[content], &[]).unwrap();
433 assert_eq!(
434 settings,
435 ModelineSettings {
436 tab_size: Some(NonZeroU32::new(6).unwrap()),
437 auto_indent: Some(false),
438 indent_size: Some(NonZeroU32::new(3).unwrap()),
439 ..Default::default()
440 }
441 );
442 }
443
444 #[test]
445 fn test_vim_modeline_first_form() {
446 // Examples from vim specification: vi:noai:sw=3 ts=6
447 let content = " vi:noai:sw=3 ts=6 ";
448 let settings = parse_modeline(&[content], &[]).unwrap();
449 assert_eq!(
450 settings,
451 ModelineSettings {
452 tab_size: Some(NonZeroU32::new(6).unwrap()),
453 auto_indent: Some(false),
454 indent_size: Some(NonZeroU32::new(3).unwrap()),
455 ..Default::default()
456 }
457 );
458
459 // Test with filetype
460 let content = "vim:ft=python:ts=8:noet";
461 let settings = parse_modeline(&[content], &[]).unwrap();
462 assert_eq!(
463 settings,
464 ModelineSettings {
465 mode: Some("python".to_string()),
466 tab_size: Some(NonZeroU32::new(8).unwrap()),
467 hard_tabs: Some(true),
468 ..Default::default()
469 }
470 );
471
472 // Using `ex:` should only be interpreted when whitespace separates it
473 // from the beginning of the line.
474 let content = "ex: filetype=shell";
475 assert_eq!(parse_modeline(&[content], &[]), None);
476
477 let content = "\tex: filetype=shell";
478 let settings = parse_modeline(&[content], &[]).unwrap();
479 assert_eq!(
480 settings,
481 ModelineSettings {
482 mode: Some("shell".to_string()),
483 ..Default::default()
484 }
485 );
486 }
487
488 #[test]
489 fn test_vim_modeline_second_form() {
490 // Examples from vim specification: /* vim: set ai tw=75: */
491 let content = "/* vim: set ai tw=75: */";
492 let settings = parse_modeline(&[content], &[]).unwrap();
493 assert_eq!(
494 settings,
495 ModelineSettings {
496 auto_indent: Some(true),
497 preferred_line_length: Some(NonZeroU32::new(75).unwrap()),
498 ..Default::default()
499 }
500 );
501
502 // Test with 'Vim:' (capital V)
503 let content = "/* Vim: set ai tw=75: */";
504 let settings = parse_modeline(&[content], &[]).unwrap();
505 assert_eq!(
506 settings,
507 ModelineSettings {
508 auto_indent: Some(true),
509 preferred_line_length: Some(NonZeroU32::new(75).unwrap()),
510 ..Default::default()
511 }
512 );
513
514 // Test 'se' shorthand
515 let content = "// vi: se ft=c ts=4:";
516 let settings = parse_modeline(&[content], &[]).unwrap();
517 assert_eq!(
518 settings,
519 ModelineSettings {
520 mode: Some("c".to_string()),
521 tab_size: Some(NonZeroU32::new(4).unwrap()),
522 ..Default::default()
523 }
524 );
525
526 // Test complex modeline with encoding
527 let content = "# vim: set ft=python ts=4 sw=4 et encoding=utf-8:";
528 let settings = parse_modeline(&[content], &[]).unwrap();
529 assert_eq!(
530 settings,
531 ModelineSettings {
532 mode: Some("python".to_string()),
533 tab_size: Some(NonZeroU32::new(4).unwrap()),
534 hard_tabs: Some(false),
535 indent_size: Some(NonZeroU32::new(4).unwrap()),
536 vim_extra_variables: vec![("encoding".to_string(), Some("utf-8".to_string()))],
537 ..Default::default()
538 }
539 );
540 }
541
542 #[test]
543 fn test_vim_modeline_edge_cases() {
544 // Test modeline at start of line (compatibility with version 3.0)
545 let content = "vi:ts=2:et";
546 let settings = parse_modeline(&[content], &[]).unwrap();
547 assert_eq!(
548 settings,
549 ModelineSettings {
550 tab_size: Some(NonZeroU32::new(2).unwrap()),
551 hard_tabs: Some(false),
552 ..Default::default()
553 }
554 );
555
556 // Test vim at start of line
557 let content = "vim:ft=rust:noet";
558 let settings = parse_modeline(&[content], &[]).unwrap();
559 assert_eq!(
560 settings,
561 ModelineSettings {
562 mode: Some("rust".to_string()),
563 hard_tabs: Some(true),
564 ..Default::default()
565 }
566 );
567
568 // Test mixed boolean flags
569 let content = "vim: set wrap noet ts=8:";
570 let settings = parse_modeline(&[content], &[]).unwrap();
571 assert_eq!(
572 settings,
573 ModelineSettings {
574 tab_size: Some(NonZeroU32::new(8).unwrap()),
575 hard_tabs: Some(true),
576 vim_extra_variables: vec![("wrap".to_string(), None)],
577 ..Default::default()
578 }
579 );
580 }
581
582 #[test]
583 fn test_vim_modeline_invalid_cases() {
584 // Test malformed options are ignored gracefully
585 let content = "vim: set ts=invalid ft=rust:";
586 let settings = parse_modeline(&[content], &[]).unwrap();
587 assert_eq!(
588 settings,
589 ModelineSettings {
590 mode: Some("rust".to_string()),
591 ..Default::default()
592 }
593 );
594
595 // Test empty modeline content - this should still work as there might be options
596 let content = "vim: set :";
597 // This should return None because there are no actual options
598 let result = parse_modeline(&[content], &[]);
599 assert!(result.is_none(), "Expected None but got: {:?}", result);
600
601 // Test modeline without proper format
602 let content = "not a modeline";
603 assert!(parse_modeline(&[content], &[]).is_none());
604
605 // Test word that looks like modeline but isn't
606 let content = "example: this could be confused with ex:";
607 assert!(parse_modeline(&[content], &[]).is_none());
608 }
609
610 #[test]
611 fn test_vim_language_mapping() {
612 // Test vim-specific language mappings
613 let content = "vim: set ft=sh:";
614 let settings = parse_modeline(&[content], &[]).unwrap();
615 assert_eq!(settings.mode, Some("sh".to_string()));
616
617 let content = "vim: set ft=golang:";
618 let settings = parse_modeline(&[content], &[]).unwrap();
619 assert_eq!(settings.mode, Some("golang".to_string()));
620
621 let content = "vim: set filetype=js:";
622 let settings = parse_modeline(&[content], &[]).unwrap();
623 assert_eq!(settings.mode, Some("js".to_string()));
624 }
625
626 #[test]
627 fn test_vim_extra_variables() {
628 // Test that unknown vim options are stored as extra variables
629 let content = "vim: set foldmethod=marker conceallevel=2 custom=value:";
630 let settings = parse_modeline(&[content], &[]).unwrap();
631
632 assert!(
633 settings
634 .vim_extra_variables
635 .contains(&("foldmethod".to_string(), Some("marker".to_string())))
636 );
637 assert!(
638 settings
639 .vim_extra_variables
640 .contains(&("conceallevel".to_string(), Some("2".to_string())))
641 );
642 assert!(
643 settings
644 .vim_extra_variables
645 .contains(&("custom".to_string(), Some("value".to_string())))
646 );
647 }
648
649 #[test]
650 fn test_modeline_position() {
651 // Test modeline in first lines
652 let first_lines = ["#!/bin/bash", "# vim: set ft=bash ts=4:"];
653 let settings = parse_modeline(&first_lines, &[]).unwrap();
654 assert_eq!(settings.mode, Some("bash".to_string()));
655
656 // Test modeline in last lines
657 let last_lines = ["", "/* vim: set ft=c: */"];
658 let settings = parse_modeline(&[], &last_lines).unwrap();
659 assert_eq!(settings.mode, Some("c".to_string()));
660
661 // Test no modeline found
662 let content = ["regular content", "no modeline here"];
663 assert!(parse_modeline(&content, &content).is_none());
664 }
665
666 #[test]
667 fn test_vim_modeline_version_checks() {
668 // Note: Current implementation doesn't support version checks yet
669 // These are tests for future implementation based on vim spec
670
671 // Test version-specific modelines (currently ignored in our implementation)
672 let content = "/* vim700: set foldmethod=marker */";
673 // Should be ignored for now since we don't support version checks
674 assert!(parse_modeline(&[content], &[]).is_none());
675
676 let content = "/* vim>702: set cole=2: */";
677 // Should be ignored for now since we don't support version checks
678 assert!(parse_modeline(&[content], &[]).is_none());
679 }
680
681 #[test]
682 fn test_vim_modeline_colon_escaping() {
683 // Test colon escaping as mentioned in vim spec
684
685 // According to vim spec: "if you want to include a ':' in a set command precede it with a '\'"
686 let content = r#"/* vim: set fdm=expr fde=getline(v\:lnum)=~'{'?'>1'\:'1': */"#;
687
688 let result = parse_modeline(&[content], &[]).unwrap();
689
690 // The modeline should parse fdm=expr and fde=getline(v:lnum)=~'{'?'>1':'1'
691 // as extra variables since they're not recognized settings
692 assert_eq!(result.vim_extra_variables.len(), 2);
693 assert_eq!(
694 result.vim_extra_variables[0],
695 ("fdm".to_string(), Some("expr".to_string()))
696 );
697 assert_eq!(
698 result.vim_extra_variables[1],
699 (
700 "fde".to_string(),
701 Some("getline(v:lnum)=~'{'?'>1':'1'".to_string())
702 )
703 );
704 }
705
706 #[test]
707 fn test_vim_modeline_whitespace_requirements() {
708 // Test whitespace requirements from vim spec
709
710 // Valid: whitespace before vi/vim
711 let content = " vim: set ft=rust:";
712 assert!(parse_modeline(&[content], &[]).is_some());
713
714 // Valid: tab before vi/vim
715 let content = "\tvim: set ft=rust:";
716 assert!(parse_modeline(&[content], &[]).is_some());
717
718 // Valid: vi/vim at start of line (compatibility)
719 let content = "vim: set ft=rust:";
720 assert!(parse_modeline(&[content], &[]).is_some());
721 }
722
723 #[test]
724 fn test_vim_modeline_comprehensive_examples() {
725 // Real-world examples from vim documentation and common usage
726
727 // Python example
728 let content = "# vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4:";
729 let settings = parse_modeline(&[content], &[]).unwrap();
730 assert_eq!(settings.hard_tabs, Some(false));
731 assert_eq!(settings.tab_size, Some(NonZeroU32::new(4).unwrap()));
732
733 // C example with multiple options
734 let content = "/* vim: set ts=8 sw=8 noet ai cindent: */";
735 let settings = parse_modeline(&[content], &[]).unwrap();
736 assert_eq!(settings.tab_size, Some(NonZeroU32::new(8).unwrap()));
737 assert_eq!(settings.hard_tabs, Some(true));
738 assert!(
739 settings
740 .vim_extra_variables
741 .contains(&("cindent".to_string(), None))
742 );
743
744 // Shell script example
745 let content = "# vi: set ft=sh ts=2 sw=2 et:";
746 let settings = parse_modeline(&[content], &[]).unwrap();
747 assert_eq!(settings.mode, Some("sh".to_string()));
748 assert_eq!(settings.tab_size, Some(NonZeroU32::new(2).unwrap()));
749 assert_eq!(settings.hard_tabs, Some(false));
750
751 // First form colon-separated
752 let content = "vim:ft=xml:ts=2:sw=2:et";
753 let settings = parse_modeline(&[content], &[]).unwrap();
754 assert_eq!(settings.mode, Some("xml".to_string()));
755 assert_eq!(settings.tab_size, Some(NonZeroU32::new(2).unwrap()));
756 assert_eq!(settings.hard_tabs, Some(false));
757 }
758
759 #[test]
760 fn test_combined_emacs_vim_detection() {
761 // Test that both emacs and vim modelines can be detected in the same file
762
763 let first_lines = [
764 "#!/usr/bin/env python3",
765 "# -*- require-final-newline: t; -*-",
766 "# vim: set ft=python ts=4 sw=4 et:",
767 ];
768
769 // Should find the emacs modeline first (with coding)
770 let settings = parse_modeline(&first_lines, &[]).unwrap();
771 assert_eq!(settings.ensure_final_newline, Some(true));
772 assert_eq!(settings.tab_size, None);
773
774 // Test vim-only content
775 let vim_only = ["# vim: set ft=python ts=4 sw=4 et:"];
776 let settings = parse_modeline(&vim_only, &[]).unwrap();
777 assert_eq!(settings.mode, Some("python".to_string()));
778 assert_eq!(settings.tab_size, Some(NonZeroU32::new(4).unwrap()));
779 assert_eq!(settings.hard_tabs, Some(false));
780 }
781}
782