Skip to content

Commit 64a89d0

Browse files
committed
also support basic vim modelines
1 parent e5fe2c4 commit 64a89d0

File tree

2 files changed

+195
-10
lines changed

2 files changed

+195
-10
lines changed

helix-core/src/modeline.rs

+183-5
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@ use crate::{LineEnding, RopeSlice};
99
// 5 is the vim default
1010
const LINES_TO_CHECK: usize = 5;
1111

12-
static MODELINE_REGEX: Lazy<rope::Regex> =
12+
static HELIX_MODELINE_REGEX: Lazy<rope::Regex> =
1313
Lazy::new(|| rope::Regex::new(r"^(.{0,100}\s{1,100})?helix:").unwrap());
14-
static MODELINE_OPTION_REGEX: Lazy<rope::Regex> =
14+
static HELIX_MODELINE_OPTION_REGEX: Lazy<rope::Regex> =
1515
Lazy::new(|| rope::Regex::new(r"[a-zA-Z0-9_-]{1,100}(?:=[a-zA-Z0-9_-]{1,100})").unwrap());
16+
static VIM_MODELINE_REGEX: Lazy<rope::Regex> = Lazy::new(|| {
17+
rope::Regex::new(
18+
r"^(.{0,100}\s{1,100})?(vi|[vV]im[<=>]?\d{0,100}|ex):\s{0,100}(set?\s{1,100})?",
19+
)
20+
.unwrap()
21+
});
22+
static VIM_MODELINE_OPTION_REGEX: Lazy<rope::Regex> =
23+
Lazy::new(|| rope::Regex::new(r"[a-zA-Z0-9_-]{1,100}(?:=(?:\\:|[^:\s]){0,1000})?").unwrap());
1624

1725
#[derive(Default, Debug, Eq, PartialEq)]
1826
pub struct Modeline {
@@ -49,9 +57,9 @@ impl Modeline {
4957
}
5058

5159
fn parse_from_line(&mut self, mut line: RopeSlice) {
52-
if let Some(pos) = MODELINE_REGEX.find(line.regex_input()) {
60+
if let Some(pos) = HELIX_MODELINE_REGEX.find(line.regex_input()) {
5361
line = line.slice(line.byte_to_char(pos.end())..);
54-
while let Some(opt_pos) = MODELINE_OPTION_REGEX.find(line.regex_input()) {
62+
while let Some(opt_pos) = HELIX_MODELINE_OPTION_REGEX.find(line.regex_input()) {
5563
let option =
5664
Cow::from(line.slice(
5765
line.byte_to_char(opt_pos.start())..line.byte_to_char(opt_pos.end()),
@@ -83,10 +91,63 @@ impl Modeline {
8391
let whitespace = line.chars().take_while(|c| char::is_whitespace(*c)).count();
8492
line = line.slice(whitespace..);
8593
}
94+
} else if let Some(pos) = VIM_MODELINE_REGEX.find(line.regex_input()) {
95+
line = line.slice(line.byte_to_char(pos.end())..);
96+
while let Some(opt_pos) = VIM_MODELINE_OPTION_REGEX.find(line.regex_input()) {
97+
let option =
98+
Cow::from(line.slice(
99+
line.byte_to_char(opt_pos.start())..line.byte_to_char(opt_pos.end()),
100+
));
101+
let mut parts = option.as_ref().splitn(2, '=');
102+
match parts.next().unwrap() {
103+
"ft" | "filetype" => {
104+
if let Some(val) = parts.next() {
105+
self.language = Some(val.to_string());
106+
}
107+
}
108+
"sw" | "shiftwidth" => {
109+
if let Some(val) = parts.next().and_then(|val| val.parse().ok()) {
110+
if self.indent_style != Some(IndentStyle::Tabs) {
111+
self.indent_style = Some(IndentStyle::Spaces(val));
112+
}
113+
}
114+
}
115+
"ff" | "fileformat" => {
116+
if let Some(val) = parts.next() {
117+
self.line_ending = vim_ff_to_helix_line_ending(val);
118+
}
119+
}
120+
"noet" | "noexpandtab" => {
121+
self.indent_style = Some(IndentStyle::Tabs);
122+
}
123+
"et" | "expandtab" => {
124+
if !matches!(self.indent_style, Some(IndentStyle::Spaces(_))) {
125+
self.indent_style = Some(IndentStyle::Spaces(0));
126+
}
127+
}
128+
_ => {}
129+
}
130+
line = line.slice(line.byte_to_char(opt_pos.end())..);
131+
let whitespace = line
132+
.chars()
133+
.take_while(|c| char::is_whitespace(*c) || *c == ':')
134+
.count();
135+
line = line.slice(whitespace..);
136+
}
86137
}
87138
}
88139
}
89140

141+
fn vim_ff_to_helix_line_ending(val: &str) -> Option<LineEnding> {
142+
match val {
143+
"dos" => Some(LineEnding::Crlf),
144+
"unix" => Some(LineEnding::LF),
145+
#[cfg(feature = "unicode-lines")]
146+
"mac" => Some(LineEnding::CR),
147+
_ => None,
148+
}
149+
}
150+
90151
#[cfg(test)]
91152
mod test {
92153
use super::*;
@@ -145,11 +206,128 @@ mod test {
145206
line_ending: Some(LineEnding::Crlf),
146207
},
147208
),
209+
(
210+
"vi:noai:sw=3 ts=6",
211+
Modeline {
212+
indent_style: Some(IndentStyle::Spaces(3)),
213+
..Default::default()
214+
},
215+
),
216+
(
217+
"vim: tw=77",
218+
Modeline {
219+
..Default::default()
220+
},
221+
),
222+
(
223+
"/* vim: set ai sw=5: */",
224+
Modeline {
225+
indent_style: Some(IndentStyle::Spaces(5)),
226+
..Default::default()
227+
},
228+
),
229+
(
230+
"# vim: set noexpandtab:",
231+
Modeline {
232+
indent_style: Some(IndentStyle::Tabs),
233+
..Default::default()
234+
},
235+
),
236+
(
237+
"# vim: set expandtab:",
238+
Modeline {
239+
indent_style: Some(IndentStyle::Spaces(0)),
240+
..Default::default()
241+
},
242+
),
243+
(
244+
"// vim: noai:ts=4:sw=4",
245+
Modeline {
246+
indent_style: Some(IndentStyle::Spaces(4)),
247+
..Default::default()
248+
},
249+
),
250+
(
251+
"/* vim: set noai ts=4 sw=4: */",
252+
Modeline {
253+
indent_style: Some(IndentStyle::Spaces(4)),
254+
..Default::default()
255+
},
256+
),
257+
(
258+
"/* vim: set fdm=expr ft=c fde=getline(v\\:lnum)=~'{'?'>1'\\:'1' sw=4: */",
259+
Modeline {
260+
language: Some("c".to_string()),
261+
indent_style: Some(IndentStyle::Spaces(4)),
262+
..Default::default()
263+
},
264+
),
265+
(
266+
"/* vim: set ts=8 sw=4 tw=0 noet : */",
267+
Modeline {
268+
indent_style: Some(IndentStyle::Tabs),
269+
..Default::default()
270+
},
271+
),
272+
(
273+
"vim:ff=unix ts=4 sw=4",
274+
Modeline {
275+
indent_style: Some(IndentStyle::Spaces(4)),
276+
line_ending: Some(LineEnding::LF),
277+
..Default::default()
278+
},
279+
),
280+
(
281+
"vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:",
282+
Modeline {
283+
language: Some("help".to_string()),
284+
indent_style: Some(IndentStyle::Spaces(2)),
285+
..Default::default()
286+
},
287+
),
288+
(
289+
"# vim: ft=zsh sw=2 ts=2 et",
290+
Modeline {
291+
language: Some("zsh".to_string()),
292+
indent_style: Some(IndentStyle::Spaces(2)),
293+
..Default::default()
294+
},
295+
),
296+
(
297+
"# vim:ft=sh:",
298+
Modeline {
299+
language: Some("sh".to_string()),
300+
..Default::default()
301+
},
302+
),
303+
(
304+
"\" vim:ts=8:sts=4:sw=4:expandtab:ft=vim",
305+
Modeline {
306+
language: Some("vim".to_string()),
307+
indent_style: Some(IndentStyle::Spaces(4)),
308+
..Default::default()
309+
},
310+
),
311+
(
312+
"\" vim: ts=8 noet tw=100 sw=8 sts=0 ft=vim isk+=-",
313+
Modeline {
314+
language: Some("vim".to_string()),
315+
indent_style: Some(IndentStyle::Tabs),
316+
..Default::default()
317+
},
318+
),
319+
(
320+
"; vim:ft=gitconfig:",
321+
Modeline {
322+
language: Some("gitconfig".to_string()),
323+
..Default::default()
324+
},
325+
),
148326
];
149327
for (line, expected) in tests {
150328
let mut got = Modeline::default();
151329
got.parse_from_line(line.into());
152-
assert_eq!(got, expected);
330+
assert_eq!(got, expected, "{line}");
153331
}
154332
}
155333
}

helix-view/src/document.rs

+12-5
Original file line numberDiff line numberDiff line change
@@ -1101,14 +1101,21 @@ impl Document {
11011101
/// configured in `languages.toml`, with a fallback to tabs if it isn't specified. Line ending
11021102
/// is likewise auto-detected, and will remain unchanged if no line endings were detected.
11031103
pub fn detect_indent_and_line_ending(&mut self, modeline: &Modeline) {
1104-
self.indent_style = modeline
1105-
.indent_style()
1106-
.or_else(|| auto_detect_indent_style(&self.text))
1107-
.unwrap_or_else(|| {
1104+
let detect_indent_style = || {
1105+
auto_detect_indent_style(&self.text).unwrap_or_else(|| {
11081106
self.language_config()
11091107
.and_then(|config| config.indent.as_ref())
11101108
.map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
1111-
});
1109+
})
1110+
};
1111+
self.indent_style = match modeline.indent_style() {
1112+
Some(IndentStyle::Spaces(0)) => match detect_indent_style() {
1113+
IndentStyle::Tabs => IndentStyle::Spaces(self.tab_width().try_into().unwrap_or(4)),
1114+
IndentStyle::Spaces(n) => IndentStyle::Spaces(n),
1115+
},
1116+
Some(style) => style,
1117+
None => detect_indent_style(),
1118+
};
11121119
if let Some(line_ending) = modeline
11131120
.line_ending()
11141121
.or_else(|| auto_detect_line_ending(&self.text))

0 commit comments

Comments
 (0)