From 7d41550a23fc3506a37016d5bc362144fb00c080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 8 Jan 2021 16:15:12 +0900 Subject: [PATCH] indent: refactor logic to be more correct. Thanks to atom-sane-indentation, nvim-treesitter and tree-sitter-indent.el for inspiration. --- helix-core/src/indent.rs | 235 +++++++++++++++++++++++++++++-------- helix-term/src/commands.rs | 2 + 2 files changed, 189 insertions(+), 48 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 6b9a1ab17..7fbe7f82e 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -27,7 +27,8 @@ fn indent_level_for_line(line: RopeSlice) -> usize { fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option { let tree = syntax.root_layer.tree.as_ref().unwrap(); - let mut node = match tree.root_node().named_descendant_for_byte_range(pos, pos) { + // named_descendant + let mut node = match tree.root_node().descendant_for_byte_range(pos, pos) { Some(node) => node, None => return None, }; @@ -43,91 +44,149 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option) -> usize { - let node = match node { - Some(node) => node, - None => return 0, - }; - - let parent = match node.parent() { - Some(node) => node, - None => return 0, - }; - +fn walk(node: Option, newline: bool) -> usize { let mut increment = 0; // Hardcoded for rust for now let indent_scopes = &[ - "block", - "function_item", - "closure_expression", + // indent except first or block? "while_expression", "for_expression", "loop_expression", "if_expression", "if_let_expression", - "binary_expression", "match_expression", "match_arm", - // - "struct_item", - "enum_item", - "impl_item", - // - "mod_item", ]; - // let indent_except_first_scopes = &[]; + // this is for multiline things, such as: + // self.method() + // .chain() + // .chain() + // where the first line isn't indented + let indent_except_first_scopes = &[ + "block", + "arguments", + "declaration_list", + "field_declaration_list", + "enum_variant_list", + // "function_item", + // "call_expression", + // "closure_expression", + "binary_expression", + "field_expression", + // + "where_predicate", // where_clause instead? + ]; - let not_first_sibling = node.next_sibling().is_some(); - let not_last_sibling = node.prev_sibling().is_some(); - let not_first_or_last_sibling = not_first_sibling && not_last_sibling; + let outdent = &["}", "]", ")"]; - let parent_kind = parent.kind(); - let is_scope = indent_scopes.iter().any(|scope| scope == &parent_kind); + let mut node = match node { + Some(node) => node, + None => return 0, + }; - // && not_first_or_last_sibling - if is_scope { - increment += 1 + // if we're calculating indentation for a brand new line then the current node will become the + // parent node. We need to take it's indentation level into account too. + let node_kind = node.kind(); + if newline + && (indent_scopes.contains(&node_kind) || indent_except_first_scopes.contains(&node_kind)) + { + increment += 1; } - // if last_scope && increment > 0 && ...{ ignore } + while let Some(parent) = node.parent() { + let not_first_sibling = node.prev_sibling().is_some(); + let not_last_sibling = node.next_sibling().is_some(); + let not_first_or_last_sibling = not_first_sibling && not_last_sibling; - walk(Some(parent)) + increment + let parent_kind = parent.kind(); + + // println!( + // "name: {}\tparent: {}\trange:\t{} {}\tfirst={:?}\tlast={:?}", + // node.kind(), + // parent.kind(), + // node.range().start_point, + // node.range().end_point, + // node.prev_sibling().is_none(), + // node.next_sibling().is_none(), + // ); + + if outdent.contains(&node.kind()) { + // we outdent by skipping the rules for the current level and jumping up + node = parent; + continue; + } + + let is_scope = indent_scopes.contains(&parent_kind); + + // && not_first_or_last_sibling + if is_scope && not_first_or_last_sibling { + // println!("is_scope {}", parent_kind); + increment += 1 + } + + let is_scope = indent_except_first_scopes.contains(&parent_kind); + + // && not_first_sibling + if is_scope && not_first_sibling { + // println!("is_scope_except_first {}", parent_kind); + increment += 1 + } + + // if last_scope && increment > 0 && ...{ ignore } + + node = parent; + } + + increment } -fn find_first_non_whitespace_char(state: &State, line_num: usize) -> usize { +fn find_first_non_whitespace_char(state: &State, line_num: usize) -> Option { let line = state.doc.line(line_num); let mut start = state.doc.line_to_char(line_num); // find first non-whitespace char for ch in line.chars() { // TODO: could use memchr with chunks? - if ch != ' ' && ch != '\t' { - break; + if ch != ' ' && ch != '\t' && ch != '\n' { + return Some(start); } start += 1; } - start + + None } fn suggested_indent_for_line(syntax: Option<&Syntax>, state: &State, line_num: usize) -> usize { let line = state.doc.line(line_num); let current = indent_level_for_line(line); - let start = find_first_non_whitespace_char(state, line_num); + if let Some(start) = find_first_non_whitespace_char(state, line_num) { + return suggested_indent_for_pos(syntax, state, start, false); + }; - suggested_indent_for_pos(syntax, state, start) + // if the line is blank, indent should be zero + 0 } -pub fn suggested_indent_for_pos(syntax: Option<&Syntax>, state: &State, pos: usize) -> usize { +// TODO: two usecases: if we are triggering this for a new, blank line: +// - it should return 0 when mass indenting stuff +// - it should look up the wrapper node and count it too when we press o/O +pub fn suggested_indent_for_pos( + syntax: Option<&Syntax>, + state: &State, + pos: usize, + new_line: bool, +) -> usize { if let Some(syntax) = syntax { let byte_start = state.doc.char_to_byte(pos); let node = get_highest_syntax_node_at_bytepos(syntax, byte_start); - let indentation = walk(node); + let indentation = walk(node, new_line); // special case for comments + // println!("------------"); // if preserve_leading_whitespace indentation @@ -155,11 +214,52 @@ fn test_indent_level() { #[test] fn test_suggested_indent_for_line() { let doc = Rope::from( - "mod test { + " +mod test { fn hello_world() { - 1 + 1 + 1 + 1; + + let does_indentation_work = 1; + + let test_function = function_with_param(this_param, + that_param + ); + + let test_function = function_with_param( + this_param, + that_param + ); + + let test_function = function_with_proper_indent(param1, + param2, + ); + + let selection = Selection::new( + changes + .clone() + .map(|(start, end, text): (usize, usize, Option)| { + let len = text.map(|text| text.len()).unwrap() - 1; // minus newline + let pos = start + len; + Range::new(pos, pos) + }) + .collect(), + 0, + ); + + return; } } + +impl MyTrait for YourType +where + A: TraitB + TraitC, + D: TraitE + TraitF, +{ + +} +#[test] +// + ", ); @@ -171,10 +271,49 @@ fn hello_world() { let highlight_config = language_config.highlight_config(&[]).unwrap().unwrap(); let syntax = Syntax::new(&state.doc, highlight_config.clone()); - assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 0), 0); // mod - assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 1), 1); // fn - assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 2), 2); // 1 + 1 - assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 4), 1); // } - assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 5), 0); // } + // { + // { + // 1 + 1 + // } + // } + // assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 1), 0); // { + // assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 2), 1); // { + // assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 3), 2); // + // assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 4), 1); // } + // assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 5), 0); // } + + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 1), 0); // mod + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 2), 1); // fn + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 3), 2); // 1 + 1 + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 4), 0); // + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 5), 2); // does_indentation_work + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 7), 2); // let test_function + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 8), 3); // that_param + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 9), 2); // ); + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 10), 0); // + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 11), 2); // let test_function + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 12), 3); // this_param + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 13), 3); // that_param + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 14), 2); // ); + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 15), 0); // + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 16), 2); // let test_function + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 17), 3); // param2 + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 18), 2); // ); + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 20), 2); // let selection + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 21), 3); // changes + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 22), 4); // clone() + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 23), 4); // map() + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 24), 5); // let len + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 25), 5); // let pos + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 26), 5); // Range + + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 27), 4); // }) + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 28), 4); // .collect(), + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 29), 3); // 0, + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 30), 2); // }) + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 31), 0); // + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 32), 2); // return; + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 33), 1); // } + assert_eq!(suggested_indent_for_line(Some(&syntax), &state, 34), 0); // } } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 8216437d5..a11c31454 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -535,6 +535,7 @@ pub fn open_below(cx: &mut Context) { cx.view.doc.syntax.as_ref(), &cx.view.doc.state, index, + true, ); let indent = " ".repeat(TAB_WIDTH).repeat(indent_level); let mut text = String::with_capacity(1 + indent.len()); @@ -654,6 +655,7 @@ pub fn insert_newline(cx: &mut Context) { cx.view.doc.syntax.as_ref(), &cx.view.doc.state, range.head, + true, ); let indent = " ".repeat(TAB_WIDTH).repeat(indent_level); let mut text = String::with_capacity(1 + indent.len());