diff --git a/helix-core/tests/data/indent/cpp.cpp b/helix-core/tests/data/indent/cpp.cpp new file mode 100644 index 00000000..6e7f3a88 --- /dev/null +++ b/helix-core/tests/data/indent/cpp.cpp @@ -0,0 +1,48 @@ +std::vector +fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4, + char* parm5, bool parm6); + +std::vector +fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4, + char* parm5, bool parm6) { + auto lambda = []() { + return 0; + }; + auto lambda_with_a_really_long_name_that_uses_a_whole_line + = [](int some_more_aligned_parameters, + std::string parm2) { + do_smth(); + }; + if (brace_on_same_line) { + do_smth(); + } else if (brace_on_next_line) + { + do_smth(); + } else if (another_condition) { + do_smth(); + } + else { + do_smth(); + } + if (inline_if_statement) + do_smth(); + if (another_inline_if_statement) + return [](int parm1, char* parm2) { + this_is_a_really_pointless_lambda(); + }; + + switch (var) { + case true: + return -1; + case false: + return 42; + } +} + +class MyClass : public MyBaseClass { +public: + MyClass(); + void public_fn(); +private: + super_secret_private_fn(); +} diff --git a/helix-core/tests/data/indent/indent.rs b/helix-core/tests/data/indent/indent.rs deleted file mode 120000 index 2ac16cf9..00000000 --- a/helix-core/tests/data/indent/indent.rs +++ /dev/null @@ -1 +0,0 @@ -../../../src/indent.rs \ No newline at end of file diff --git a/helix-core/tests/data/indent/languages.toml b/helix-core/tests/data/indent/languages.toml index 3206f124..fa02e451 100644 --- a/helix-core/tests/data/indent/languages.toml +++ b/helix-core/tests/data/indent/languages.toml @@ -11,3 +11,16 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "rust" source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" } + +[[language]] +name = "cpp" +scope = "source.cpp" +injection-regex = "cpp" +file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H"] +roots = [] +comment-token = "//" +indent = { tab-width = 2, unit = " " } + +[[grammar]] +name = "cpp" +source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "2d2c4aee8672af4c7c8edff68e7dd4c07e88d2b1" } diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs index 1dec9989..26010c90 100644 --- a/helix-core/tests/indent.rs +++ b/helix-core/tests/indent.rs @@ -1,20 +1,122 @@ use helix_core::{ indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle}, - syntax::Loader, + syntax::{Configuration, Loader}, Syntax, }; -use std::path::PathBuf; +use ropey::Rope; +use std::{ops::Range, path::PathBuf, process::Command}; #[test] fn test_treesitter_indent_rust() { - test_treesitter_indent("rust.rs", "source.rust"); + standard_treesitter_test("rust.rs", "source.rust"); } + +#[test] +fn test_treesitter_indent_cpp() { + standard_treesitter_test("cpp.cpp", "source.cpp"); +} + #[test] -fn test_treesitter_indent_rust_2() { - test_treesitter_indent("indent.rs", "source.rust"); - // TODO Use commands.rs as indentation test. - // Currently this fails because we can't align the parameters of a closure yet - // test_treesitter_indent("commands.rs", "source.rust"); +fn test_treesitter_indent_rust_helix() { + // We pin a specific git revision to prevent unrelated changes from causing the indent tests to fail. + // Ideally, someone updates this once in a while and fixes any errors that occur. + let rev = "af382768cdaf89ff547dbd8f644a1bddd90e7c8f"; + let files = Command::new("git") + .args([ + "ls-tree", + "-r", + "--name-only", + "--full-tree", + rev, + "helix-term/src", + ]) + .output() + .unwrap(); + let files = String::from_utf8(files.stdout).unwrap(); + + let ignored_files = vec![ + // Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting + "helix-term/src/health.rs", + ]; + + for file in files.split_whitespace() { + if ignored_files.contains(&file) { + continue; + } + let ignored_lines: Vec> = match file { + "helix-term/src/application.rs" => vec![ + // We can't handle complicated indent rules inside macros (`json!` in this case) since + // the tree-sitter grammar only parses them as `token_tree` and `identifier` nodes. + 1045..1051, + ], + "helix-term/src/commands.rs" => vec![ + // This is broken because of the current handling of `call_expression` + // (i.e. having an indent query for it but outdenting again in specific cases). + // The indent query is needed to correctly handle multi-line arguments in function calls + // inside indented `field_expression` nodes (which occurs fairly often). + // + // Once we have the `@indent.always` capture type, it might be possible to just have an indent + // capture for the `arguments` field of a call expression. That could enable us to correctly + // handle this. + 2226..2230, + ], + "helix-term/src/commands/dap.rs" => vec![ + // Complex `format!` macro + 46..52, + ], + "helix-term/src/commands/lsp.rs" => vec![ + // Macro + 624..627, + // Return type declaration of a closure. `cargo fmt` adds an additional space here, + // which we cannot (yet) model with our indent queries. + 878..879, + // Same as in `helix-term/src/commands.rs` + 1335..1343, + ], + "helix-term/src/config.rs" => vec![ + // Multiline string + 146..152, + ], + "helix-term/src/keymap.rs" => vec![ + // Complex macro (see above) + 456..470, + // Multiline string without indent + 563..567, + ], + "helix-term/src/main.rs" => vec![ + // Multiline string + 44..70, + ], + "helix-term/src/ui/completion.rs" => vec![ + // Macro + 218..232, + ], + "helix-term/src/ui/editor.rs" => vec![ + // The chained function calls here are not indented, probably because of the comment + // in between. Since `cargo fmt` doesn't even attempt to format it, there's probably + // no point in trying to indent this correctly. + 342..350, + ], + "helix-term/src/ui/lsp.rs" => vec![ + // Macro + 56..61, + ], + "helix-term/src/ui/statusline.rs" => vec![ + // Same as in `helix-term/src/commands.rs` + 436..442, + 450..456, + ], + _ => Vec::new(), + }; + + let git_object = rev.to_string() + ":" + file; + let content = Command::new("git") + .args(["cat-file", "blob", &git_object]) + .output() + .unwrap(); + let doc = Rope::from_reader(&mut content.stdout.as_slice()).unwrap(); + test_treesitter_indent(file, doc, "source.rust", ignored_lines); + } } #[test] @@ -50,20 +152,41 @@ fn test_indent_level_for_line_with_spaces_and_tabs() { assert_eq!(indent_level, 2) } -fn test_treesitter_indent(file_name: &str, lang_scope: &str) { +fn indent_tests_dir() -> PathBuf { let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); test_dir.push("tests/data/indent"); + test_dir +} + +fn indent_test_path(name: &str) -> PathBuf { + let mut path = indent_tests_dir(); + path.push(name); + path +} - let mut test_file = test_dir.clone(); - test_file.push(file_name); - let test_file = std::fs::File::open(test_file).unwrap(); +fn indent_tests_config() -> Configuration { + let mut config_path = indent_tests_dir(); + config_path.push("languages.toml"); + let config = std::fs::read_to_string(config_path).unwrap(); + toml::from_str(&config).unwrap() +} + +fn standard_treesitter_test(file_name: &str, lang_scope: &str) { + let test_path = indent_test_path(file_name); + let test_file = std::fs::File::open(test_path).unwrap(); let doc = ropey::Rope::from_reader(test_file).unwrap(); + test_treesitter_indent(file_name, doc, lang_scope, Vec::new()) +} - let mut config_file = test_dir; - config_file.push("languages.toml"); - let config = std::fs::read_to_string(config_file).unwrap(); - let config = toml::from_str(&config).unwrap(); - let loader = Loader::new(config); +/// Test that all the lines in the given file are indented as expected. +/// ignored_lines is a list of (1-indexed) line ranges that are excluded from this test. +fn test_treesitter_indent( + test_name: &str, + doc: Rope, + lang_scope: &str, + ignored_lines: Vec>, +) { + let loader = Loader::new(indent_tests_config()); // set runtime path so we can find the queries 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) { std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap()); let language_config = loader.language_config_for_scope(lang_scope).unwrap(); + let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit); let highlight_config = language_config.highlight_config(&[]).unwrap(); let text = doc.slice(..); 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) { for i in 0..doc.len_lines() { let line = text.line(i); + if ignored_lines.iter().any(|range| range.contains(&(i + 1))) { + continue; + } if let Some(pos) = helix_core::find_first_non_whitespace_char(line) { - let tab_and_indent_width: usize = 4; + let tab_width: usize = 4; let suggested_indent = treesitter_indent_for_pos( indent_query, &syntax, - &IndentStyle::Spaces(tab_and_indent_width as u8), - tab_and_indent_width, - tab_and_indent_width, + &indent_style, + tab_width, + indent_style.indent_width(tab_width), text, i, text.line_to_char(i) + pos, @@ -94,7 +221,8 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) { .unwrap(); assert!( line.get_slice(..pos).map_or(false, |s| s == suggested_indent), - "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n", + "Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n", + test_name, i+1, line.slice(..line.len_chars()-1), suggested_indent,