Skip to content

Commit 6641638

Browse files
committed
patchy: auto-merge pull request helix-editor#7788
`patchy` is a tool which makes it easy to declaratively manage personal forks by automatically merging pull requests. Check it out here: https://github.com/NikitaRevenco/patchy
2 parents b47b946 + cdc53f4 commit 6641638

File tree

4 files changed

+339
-9
lines changed

4 files changed

+339
-9
lines changed

helix-core/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod indent;
1717
pub mod line_ending;
1818
pub mod macros;
1919
pub mod match_brackets;
20+
pub mod modeline;
2021
pub mod movement;
2122
pub mod object;
2223
mod position;

helix-core/src/modeline.rs

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
use std::borrow::Cow;
2+
3+
use once_cell::sync::Lazy;
4+
5+
use crate::indent::IndentStyle;
6+
use crate::regex::Regex;
7+
use crate::syntax::ModelineConfig;
8+
use crate::{LineEnding, RopeSlice};
9+
10+
// 5 is the vim default
11+
const LINES_TO_CHECK: usize = 5;
12+
const LENGTH_TO_CHECK: usize = 256;
13+
14+
static VIM_MODELINE_REGEX: Lazy<Regex> =
15+
Lazy::new(|| Regex::new(r"^(\S*\s+)?(vi|[vV]im[<=>]?\d*|ex):\s*(set?\s+)?").unwrap());
16+
static HELIX_MODELINE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\S*\s+)?helix:").unwrap());
17+
18+
#[derive(Default, Debug, Eq, PartialEq)]
19+
pub struct Modeline {
20+
language: Option<String>,
21+
indent_style: Option<IndentStyle>,
22+
line_ending: Option<LineEnding>,
23+
}
24+
25+
impl Modeline {
26+
pub fn parse(text: RopeSlice) -> Self {
27+
let mut modeline = Self::default();
28+
29+
for line in text.lines().take(LINES_TO_CHECK).chain(
30+
text.lines_at(text.len_lines())
31+
.reversed()
32+
.take(LINES_TO_CHECK),
33+
) {
34+
// can't guarantee no extra copies, since we need to regex and
35+
// regexes can't operate over chunks yet, but we can at least
36+
// limit how much we potentially need to copy because modelines
37+
// are typically quite short.
38+
if line.len_chars() > LENGTH_TO_CHECK {
39+
continue;
40+
}
41+
let line = Cow::<str>::from(line);
42+
modeline.parse_from_line(&line);
43+
}
44+
45+
modeline
46+
}
47+
48+
pub fn language(&self) -> Option<&str> {
49+
self.language.as_deref()
50+
}
51+
52+
pub fn indent_style(&self) -> Option<IndentStyle> {
53+
self.indent_style
54+
}
55+
56+
pub fn line_ending(&self) -> Option<LineEnding> {
57+
self.line_ending
58+
}
59+
60+
fn parse_from_line(&mut self, line: &str) {
61+
let mut saw_backslash = false;
62+
let split_modeline = move |c| {
63+
saw_backslash = match c {
64+
':' if !saw_backslash => return true,
65+
'\\' => true,
66+
_ => false,
67+
};
68+
c == ' ' || c == '\t'
69+
};
70+
71+
if let Some(pos) = VIM_MODELINE_REGEX.find(line) {
72+
for option in line[pos.end()..].split(split_modeline) {
73+
let parts: Vec<_> = option.split('=').collect();
74+
match parts[0] {
75+
"ft" | "filetype" => {
76+
if let Some(val) = parts.get(1) {
77+
self.language = Some(val.to_string());
78+
}
79+
}
80+
"sw" | "shiftwidth" => {
81+
if let Some(val) = parts.get(1).and_then(|val| val.parse().ok()) {
82+
if self.indent_style != Some(IndentStyle::Tabs) {
83+
self.indent_style = Some(IndentStyle::Spaces(val));
84+
}
85+
}
86+
}
87+
"ff" | "fileformat" => {
88+
if let Some(val) = parts.get(1) {
89+
self.line_ending = vim_ff_to_helix_line_ending(val);
90+
}
91+
}
92+
"noet" | "noexpandtab" => {
93+
self.indent_style = Some(IndentStyle::Tabs);
94+
}
95+
_ => {}
96+
}
97+
}
98+
}
99+
100+
if let Some(pos) = HELIX_MODELINE_REGEX.find(line) {
101+
let config = &line[pos.end()..];
102+
match toml::from_str::<ModelineConfig>(config) {
103+
Ok(modeline) => {
104+
if let Some(language) = modeline.language {
105+
self.language = Some(language);
106+
}
107+
if let Some(indent) = modeline.indent {
108+
self.indent_style = Some(IndentStyle::from_str(&indent.unit));
109+
}
110+
if let Some(line_ending) = modeline.line_ending {
111+
self.line_ending = LineEnding::from_str(&line_ending);
112+
if self.line_ending.is_none() {
113+
log::warn!("could not interpret line ending {line_ending:?}");
114+
}
115+
}
116+
}
117+
Err(e) => log::warn!("{e}"),
118+
}
119+
}
120+
}
121+
}
122+
123+
fn vim_ff_to_helix_line_ending(val: &str) -> Option<LineEnding> {
124+
match val {
125+
"dos" => Some(LineEnding::Crlf),
126+
"unix" => Some(LineEnding::LF),
127+
#[cfg(feature = "unicode-lines")]
128+
"mac" => Some(LineEnding::CR),
129+
_ => None,
130+
}
131+
}
132+
133+
#[cfg(test)]
134+
mod test {
135+
use super::*;
136+
137+
#[test]
138+
fn test_modeline_parsing() {
139+
let tests = [
140+
(
141+
"vi:noai:sw=3 ts=6",
142+
Modeline {
143+
indent_style: Some(IndentStyle::Spaces(3)),
144+
..Default::default()
145+
},
146+
),
147+
(
148+
"vim: tw=77",
149+
Modeline {
150+
..Default::default()
151+
},
152+
),
153+
(
154+
"/* vim: set ai sw=5: */",
155+
Modeline {
156+
indent_style: Some(IndentStyle::Spaces(5)),
157+
..Default::default()
158+
},
159+
),
160+
(
161+
"# vim: set noexpandtab:",
162+
Modeline {
163+
indent_style: Some(IndentStyle::Tabs),
164+
..Default::default()
165+
},
166+
),
167+
(
168+
"// vim: noai:ts=4:sw=4",
169+
Modeline {
170+
indent_style: Some(IndentStyle::Spaces(4)),
171+
..Default::default()
172+
},
173+
),
174+
(
175+
"/* vim: set noai ts=4 sw=4: */",
176+
Modeline {
177+
indent_style: Some(IndentStyle::Spaces(4)),
178+
..Default::default()
179+
},
180+
),
181+
(
182+
"/* vim: set fdm=expr ft=c fde=getline(v\\:lnum)=~'{'?'>1'\\:'1' sw=4: */",
183+
Modeline {
184+
language: Some("c".to_string()),
185+
indent_style: Some(IndentStyle::Spaces(4)),
186+
..Default::default()
187+
},
188+
),
189+
(
190+
"/* vim: set ts=8 sw=4 tw=0 noet : */",
191+
Modeline {
192+
indent_style: Some(IndentStyle::Tabs),
193+
..Default::default()
194+
},
195+
),
196+
(
197+
"vim:ff=unix ts=4 sw=4",
198+
Modeline {
199+
indent_style: Some(IndentStyle::Spaces(4)),
200+
line_ending: Some(LineEnding::LF),
201+
..Default::default()
202+
},
203+
),
204+
(
205+
"vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:",
206+
Modeline {
207+
language: Some("help".to_string()),
208+
indent_style: Some(IndentStyle::Spaces(2)),
209+
..Default::default()
210+
},
211+
),
212+
(
213+
"# vim: ft=zsh sw=2 ts=2 et",
214+
Modeline {
215+
language: Some("zsh".to_string()),
216+
indent_style: Some(IndentStyle::Spaces(2)),
217+
..Default::default()
218+
},
219+
),
220+
(
221+
"# vim:ft=sh:",
222+
Modeline {
223+
language: Some("sh".to_string()),
224+
..Default::default()
225+
},
226+
),
227+
(
228+
"\" vim:ts=8:sts=4:sw=4:expandtab:ft=vim",
229+
Modeline {
230+
language: Some("vim".to_string()),
231+
indent_style: Some(IndentStyle::Spaces(4)),
232+
..Default::default()
233+
},
234+
),
235+
(
236+
"\" vim: ts=8 noet tw=100 sw=8 sts=0 ft=vim isk+=-",
237+
Modeline {
238+
language: Some("vim".to_string()),
239+
indent_style: Some(IndentStyle::Tabs),
240+
..Default::default()
241+
},
242+
),
243+
(
244+
"; vim:ft=gitconfig:",
245+
Modeline {
246+
language: Some("gitconfig".to_string()),
247+
..Default::default()
248+
},
249+
),
250+
(
251+
"# helix: language = 'perl'",
252+
Modeline {
253+
language: Some("perl".to_string()),
254+
..Default::default()
255+
},
256+
),
257+
(
258+
"# helix: indent = { unit = ' ' }",
259+
Modeline {
260+
indent_style: Some(IndentStyle::Spaces(3)),
261+
..Default::default()
262+
},
263+
),
264+
(
265+
"# helix: indent = { unit = \"\t\" }",
266+
Modeline {
267+
indent_style: Some(IndentStyle::Tabs),
268+
..Default::default()
269+
},
270+
),
271+
(
272+
"# helix: indent = { unit = \"\\t\" }",
273+
Modeline {
274+
indent_style: Some(IndentStyle::Tabs),
275+
..Default::default()
276+
},
277+
),
278+
(
279+
"# helix: line-ending = \"\\r\\n\"",
280+
Modeline {
281+
line_ending: Some(LineEnding::Crlf),
282+
..Default::default()
283+
},
284+
),
285+
];
286+
for (line, expected) in tests {
287+
let mut got = Modeline::default();
288+
got.parse_from_line(line);
289+
assert_eq!(got, expected);
290+
}
291+
}
292+
}

helix-core/src/syntax.rs

+18
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,18 @@ pub struct LanguageConfiguration {
174174
pub persistent_diagnostic_sources: Vec<String>,
175175
}
176176

177+
/// The subset of LanguageConfig which can be read from a modeline.
178+
#[derive(Debug, Serialize, Deserialize)]
179+
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
180+
pub struct ModelineConfig {
181+
/// the language name (corresponds to language_id in LanguageConfig)
182+
pub language: Option<String>,
183+
/// the indent settings (only unit is supported in modelines)
184+
pub indent: Option<ModelineIndentationConfiguration>,
185+
/// the line ending to use (as a literal string)
186+
pub line_ending: Option<String>,
187+
}
188+
177189
#[derive(Debug, PartialEq, Eq, Hash)]
178190
pub enum FileType {
179191
/// The extension of the file, either the `Path::extension` or the full
@@ -540,6 +552,12 @@ pub struct DebuggerQuirks {
540552
pub absolute_paths: bool,
541553
}
542554

555+
#[derive(Debug, Serialize, Deserialize)]
556+
#[serde(rename_all = "kebab-case")]
557+
pub struct ModelineIndentationConfiguration {
558+
pub unit: String,
559+
}
560+
543561
#[derive(Debug, Serialize, Deserialize)]
544562
#[serde(rename_all = "kebab-case")]
545563
pub struct IndentationConfiguration {

0 commit comments

Comments
 (0)