on master help sign in
Run indentation tests on a part of the Helix source code.
b315901Daniel Ebert
helix-core/tests/data/indent/cpp.cppnew file+48
@@ -0,0 +1,48 @@
1+std::vector<std::string>
2+fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
3+ char* parm5, bool parm6);
4+
5+std::vector<std::string>
6+fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
7+ char* parm5, bool parm6) {
8+ auto lambda = []() {
9+ return 0;
10+ };
11+ auto lambda_with_a_really_long_name_that_uses_a_whole_line
12+ = [](int some_more_aligned_parameters,
13+ std::string parm2) {
14+ do_smth();
15+ };
16+ if (brace_on_same_line) {
17+ do_smth();
18+ } else if (brace_on_next_line)
19+ {
20+ do_smth();
21+ } else if (another_condition) {
22+ do_smth();
23+ }
24+ else {
25+ do_smth();
26+ }
27+ if (inline_if_statement)
28+ do_smth();
29+ if (another_inline_if_statement)
30+ return [](int parm1, char* parm2) {
31+ this_is_a_really_pointless_lambda();
32+ };
33+
34+ switch (var) {
35+ case true:
36+ return -1;
37+ case false:
38+ return 42;
39+ }
40+}
41+
42+class MyClass : public MyBaseClass {
43+public:
44+ MyClass();
45+ void public_fn();
46+private:
47+ super_secret_private_fn();
48+}
helix-core/tests/data/indent/indent.rsdeleted−1
@@ -1 +0,0 @@
1−../../../src/indent.rs
helix-core/tests/data/indent/languages.toml+13
@@ -11,3 +11,16 @@ indent = { tab-width = 4, unit = " " }
11[[grammar]]
11[[grammar]]
12name = "rust"
12name = "rust"
13source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
13source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
14+
15+[[language]]
16+name = "cpp"
17+scope = "source.cpp"
18+injection-regex = "cpp"
19+file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H"]
20+roots = []
21+comment-token = "//"
22+indent = { tab-width = 2, unit = " " }
23+
24+[[grammar]]
25+name = "cpp"
26+source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "2d2c4aee8672af4c7c8edff68e7dd4c07e88d2b1" }
helix-core/tests/indent.rs+150−22
@@ -1,20 +1,122 @@
1use helix_core::{
1use helix_core::{
2 indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
2 indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
3− syntax::Loader,
3+ syntax::{Configuration, Loader},
4 Syntax,
4 Syntax,
5};
5};
6+use ropey::Rope;
6−use std::path::PathBuf;
7+use std::{ops::Range, path::PathBuf, process::Command};
7
8
8#[test]
9#[test]
9fn test_treesitter_indent_rust() {
10fn test_treesitter_indent_rust() {
10− test_treesitter_indent("rust.rs", "source.rust");
11+ standard_treesitter_test("rust.rs", "source.rust");
11}
12}
13+
14+#[test]
15+fn test_treesitter_indent_cpp() {
16+ standard_treesitter_test("cpp.cpp", "source.cpp");
17+}
18+
12#[test]
19#[test]
13−fn test_treesitter_indent_rust_2() {
20+fn test_treesitter_indent_rust_helix() {
14− test_treesitter_indent("indent.rs", "source.rust");
21+ // We pin a specific git revision to prevent unrelated changes from causing the indent tests to fail.
15− // TODO Use commands.rs as indentation test.
22+ // Ideally, someone updates this once in a while and fixes any errors that occur.
16− // Currently this fails because we can't align the parameters of a closure yet
23+ let rev = "af382768cdaf89ff547dbd8f644a1bddd90e7c8f";
17− // test_treesitter_indent("commands.rs", "source.rust");
24+ let files = Command::new("git")
25+ .args([
26+ "ls-tree",
27+ "-r",
28+ "--name-only",
29+ "--full-tree",
30+ rev,
31+ "helix-term/src",
32+ ])
33+ .output()
34+ .unwrap();
35+ let files = String::from_utf8(files.stdout).unwrap();
36+
37+ let ignored_files = vec![
38+ // Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting
39+ "helix-term/src/health.rs",
40+ ];
41+
42+ for file in files.split_whitespace() {
43+ if ignored_files.contains(&file) {
44+ continue;
45+ }
46+ let ignored_lines: Vec<Range<usize>> = match file {
47+ "helix-term/src/application.rs" => vec![
48+ // We can't handle complicated indent rules inside macros (`json!` in this case) since
49+ // the tree-sitter grammar only parses them as `token_tree` and `identifier` nodes.
50+ 1045..1051,
51+ ],
52+ "helix-term/src/commands.rs" => vec![
53+ // This is broken because of the current handling of `call_expression`
54+ // (i.e. having an indent query for it but outdenting again in specific cases).
55+ // The indent query is needed to correctly handle multi-line arguments in function calls
56+ // inside indented `field_expression` nodes (which occurs fairly often).
57+ //
58+ // Once we have the `@indent.always` capture type, it might be possible to just have an indent
59+ // capture for the `arguments` field of a call expression. That could enable us to correctly
60+ // handle this.
61+ 2226..2230,
62+ ],
63+ "helix-term/src/commands/dap.rs" => vec![
64+ // Complex `format!` macro
65+ 46..52,
66+ ],
67+ "helix-term/src/commands/lsp.rs" => vec![
68+ // Macro
69+ 624..627,
70+ // Return type declaration of a closure. `cargo fmt` adds an additional space here,
71+ // which we cannot (yet) model with our indent queries.
72+ 878..879,
73+ // Same as in `helix-term/src/commands.rs`
74+ 1335..1343,
75+ ],
76+ "helix-term/src/config.rs" => vec![
77+ // Multiline string
78+ 146..152,
79+ ],
80+ "helix-term/src/keymap.rs" => vec![
81+ // Complex macro (see above)
82+ 456..470,
83+ // Multiline string without indent
84+ 563..567,
85+ ],
86+ "helix-term/src/main.rs" => vec![
87+ // Multiline string
88+ 44..70,
89+ ],
90+ "helix-term/src/ui/completion.rs" => vec![
91+ // Macro
92+ 218..232,
93+ ],
94+ "helix-term/src/ui/editor.rs" => vec![
95+ // The chained function calls here are not indented, probably because of the comment
96+ // in between. Since `cargo fmt` doesn't even attempt to format it, there's probably
97+ // no point in trying to indent this correctly.
98+ 342..350,
99+ ],
100+ "helix-term/src/ui/lsp.rs" => vec![
101+ // Macro
102+ 56..61,
103+ ],
104+ "helix-term/src/ui/statusline.rs" => vec![
105+ // Same as in `helix-term/src/commands.rs`
106+ 436..442,
107+ 450..456,
108+ ],
109+ _ => Vec::new(),
110+ };
111+
112+ let git_object = rev.to_string() + ":" + file;
113+ let content = Command::new("git")
114+ .args(["cat-file", "blob", &git_object])
115+ .output()
116+ .unwrap();
117+ let doc = Rope::from_reader(&mut content.stdout.as_slice()).unwrap();
118+ test_treesitter_indent(file, doc, "source.rust", ignored_lines);
119+ }
18}
120}
19
121
20#[test]
122#[test]
@@ -50,20 +152,41 @@ fn test_indent_level_for_line_with_spaces_and_tabs() {
50 assert_eq!(indent_level, 2)
152 assert_eq!(indent_level, 2)
51}
153}
52
154
53−fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
155+fn indent_tests_dir() -> PathBuf {
54 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
156 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
55 test_dir.push("tests/data/indent");
157 test_dir.push("tests/data/indent");
158+ test_dir
159+}
160+
161+fn indent_test_path(name: &str) -> PathBuf {
162+ let mut path = indent_tests_dir();
163+ path.push(name);
164+ path
165+}
56
166
57− let mut test_file = test_dir.clone();
167+fn indent_tests_config() -> Configuration {
58− test_file.push(file_name);
168+ let mut config_path = indent_tests_dir();
169+ config_path.push("languages.toml");
170+ let config = std::fs::read_to_string(config_path).unwrap();
171+ toml::from_str(&config).unwrap()
172+}
173+
174+fn standard_treesitter_test(file_name: &str, lang_scope: &str) {
175+ let test_path = indent_test_path(file_name);
59− let test_file = std::fs::File::open(test_file).unwrap();
176+ let test_file = std::fs::File::open(test_path).unwrap();
60 let doc = ropey::Rope::from_reader(test_file).unwrap();
177 let doc = ropey::Rope::from_reader(test_file).unwrap();
178+ test_treesitter_indent(file_name, doc, lang_scope, Vec::new())
179+}
61
180
62− let mut config_file = test_dir;
181+/// Test that all the lines in the given file are indented as expected.
63− config_file.push("languages.toml");
182+/// ignored_lines is a list of (1-indexed) line ranges that are excluded from this test.
64− let config = std::fs::read_to_string(config_file).unwrap();
183+fn test_treesitter_indent(
65− let config = toml::from_str(&config).unwrap();
184+ test_name: &str,
185+ doc: Rope,
186+ lang_scope: &str,
187+ ignored_lines: Vec<std::ops::Range<usize>>,
188+) {
66− let loader = Loader::new(config);
189+ let loader = Loader::new(indent_tests_config());
67
190
68 // set runtime path so we can find the queries
191 // set runtime path so we can find the queries
69 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
192 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -71,6 +194,7 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
71 std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
194 std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
72
195
73 let language_config = loader.language_config_for_scope(lang_scope).unwrap();
196 let language_config = loader.language_config_for_scope(lang_scope).unwrap();
197+ let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit);
74 let highlight_config = language_config.highlight_config(&[]).unwrap();
198 let highlight_config = language_config.highlight_config(&[]).unwrap();
75 let text = doc.slice(..);
199 let text = doc.slice(..);
76 let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
200 let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
@@ -78,14 +202,17 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
78
202
79 for i in 0..doc.len_lines() {
203 for i in 0..doc.len_lines() {
80 let line = text.line(i);
204 let line = text.line(i);
205+ if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
206+ continue;
207+ }
81 if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
208 if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
82− let tab_and_indent_width: usize = 4;
209+ let tab_width: usize = 4;
83 let suggested_indent = treesitter_indent_for_pos(
210 let suggested_indent = treesitter_indent_for_pos(
84 indent_query,
211 indent_query,
85 &syntax,
212 &syntax,
86− &IndentStyle::Spaces(tab_and_indent_width as u8),
213+ &indent_style,
87− tab_and_indent_width,
214+ tab_width,
88− tab_and_indent_width,
215+ indent_style.indent_width(tab_width),
89 text,
216 text,
90 i,
217 i,
91 text.line_to_char(i) + pos,
218 text.line_to_char(i) + pos,
@@ -94,7 +221,8 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
94 .unwrap();
221 .unwrap();
95 assert!(
222 assert!(
96 line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
223 line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
97− "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
224+ "Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
225+ test_name,
98 i+1,
226 i+1,
99 line.slice(..line.len_chars()-1),
227 line.slice(..line.len_chars()-1),
100 suggested_indent,
228 suggested_indent,