Skip to content

Commit 431899d

Browse files
elmarcoclaude
andcommitted
language: Add emacs "Local Variables:" parsing support
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
1 parent 8246340 commit 431899d

File tree

1 file changed

+107
-1
lines changed

1 file changed

+107
-1
lines changed

crates/language/src/modeline.rs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,16 @@ impl ModelineSettings {
4545
/// Parse modelines from file content.
4646
///
4747
/// Supports:
48-
/// - Emacs modelines: -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*-
48+
/// - Emacs modelines: -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*- and "Local Variables"
4949
/// - Vim modelines: vim: set ft=rust ts=4 sw=4 et:
5050
pub fn parse_modeline(first_lines: &[&str], last_lines: &[&str]) -> Option<ModelineSettings> {
5151
let mut settings = ModelineSettings::default();
5252

5353
parse_modelines(first_lines, &mut settings);
5454

55+
// Parse Emacs Local Variables in last lines
56+
parse_emacs_local_variables(last_lines, &mut settings);
57+
5558
// Also check for vim modelines in last lines if we don't have settings yet
5659
if !settings.has_settings() {
5760
parse_vim_modelines(last_lines, &mut settings);
@@ -90,6 +93,61 @@ fn parse_emacs_modeline(line: &str, settings: &mut ModelineSettings) {
9093
}
9194
}
9295

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
108+
fn 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+
93151
fn parse_emacs_key_value(part: &str, settings: &mut ModelineSettings, bare: bool) {
94152
let part = part.trim();
95153
if part.is_empty() {
@@ -266,6 +324,7 @@ fn parse_vim_settings(content: &str, settings: &mut ModelineSettings) {
266324
#[cfg(test)]
267325
mod tests {
268326
use super::*;
327+
use indoc::indoc;
269328
use pretty_assertions::assert_eq;
270329

271330
#[test]
@@ -302,6 +361,53 @@ mod tests {
302361
);
303362
}
304363

364+
#[test]
365+
fn test_emacs_last_line_parsing() {
366+
let content = indoc! {r#"
367+
# Local Variables:
368+
# compile-command: "cc foo.c -Dfoo=bar -Dhack=whatever \
369+
# -Dmumble=blaah"
370+
# End:
371+
"#}
372+
.lines()
373+
.collect::<Vec<_>>();
374+
let settings = parse_modeline(&[], &content).unwrap();
375+
assert_eq!(
376+
settings,
377+
ModelineSettings {
378+
emacs_extra_variables: vec![(
379+
"compile-command".to_string(),
380+
"\"cc foo.c -Dfoo=bar -Dhack=whatever -Dmumble=blaah\"".to_string()
381+
),],
382+
..Default::default()
383+
}
384+
);
385+
386+
let content = indoc! {"
387+
foo
388+
/* Local Variables: */
389+
/* eval: (font-lock-mode -1) */
390+
/* mode: old-c */
391+
/* mode: c */
392+
/* End: */
393+
/* mode: ignored */
394+
"}
395+
.lines()
396+
.collect::<Vec<_>>();
397+
let settings = parse_modeline(&[], &content).unwrap();
398+
assert_eq!(
399+
settings,
400+
ModelineSettings {
401+
mode: Some("c".to_string()),
402+
emacs_extra_variables: vec![(
403+
"eval".to_string(),
404+
"(font-lock-mode -1)".to_string()
405+
),],
406+
..Default::default()
407+
}
408+
);
409+
}
410+
305411
#[test]
306412
fn test_vim_modeline_parsing() {
307413
// Test second form (set format)

0 commit comments

Comments
 (0)