use crate::{ find_first_non_whitespace_char, syntax::Syntax, tree_sitter::{Node, Tree}, Rope, RopeSlice, }; /// To determine indentation of a newly inserted line, figure out the indentation at the last col /// of the previous line. fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { let mut len = 0; for ch in line.chars() { match ch { '\t' => len += tab_width, ' ' => len += 1, _ => break, } } len / tab_width } /// Find the highest syntax node at position. /// This is to identify the column where this node (e.g., an HTML closing tag) ends. fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option { let tree = syntax.tree(); // named_descendant let mut node = match tree.root_node().descendant_for_byte_range(pos, pos) { Some(node) => node, None => return None, }; while let Some(parent) = node.parent() { if parent.start_byte() == node.start_byte() { node = parent } else { break; } } Some(node) } fn calculate_indentation(node: Option, newline: bool) -> usize { let mut increment: i32 = 0; // Hardcoded for rust for now let indent_scopes = &[ // indent except first or block? "while_expression", "for_expression", "loop_expression", "if_expression", "if_let_expression", // "match_expression", // "match_arm", ]; // this is for multiline things, such as: // self.method() // .chain() // .chain() // where the first line isn't indented let indent_except_first_scopes = &[ "block", "match_block", "arguments", "declaration_list", "field_declaration_list", "enum_variant_list", // "function_item", // "closure_expression", "binary_expression", "field_expression", // "where_clause", // "use_list", ]; let outdent = &["where", "}", "]", ")"]; let mut node = match node { Some(node) => node, None => return 0, }; let mut prev_start = node.start_position().row; // 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; } 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; let parent_kind = parent.kind(); let start = parent.start_position().row; // detect deeply nested indents in the same line let starts_same_line = start == prev_start; // log::error!( // "name: {}\tparent: {}\trange:\t{} {}\tfirst={:?}\tlast={:?} start={} prev={} same_line={}", // node.kind(), // parent.kind(), // node.range().start_point, // node.range().end_point, // node.prev_sibling().is_none(), // node.next_sibling().is_none(), // node.start_position(), // prev_start, // starts_same_line // ); if outdent.contains(&node.kind()) && !starts_same_line { // we outdent by skipping the rules for the current level and jumping up // node = parent; increment -= 1; // continue; } // TODO: problem seems to be, ({ is two scopes that merge into one. // so when seeing } we're supposed to jump all the way out of both scopes, but we only do // so for one. // .map(|a| { // let len = 1; // }) if (indent_scopes.contains(&parent_kind) // && not_first_or_last_sibling || indent_except_first_scopes.contains(&parent_kind)) && !starts_same_line { // println!("is_scope {}", parent_kind); prev_start = start; increment += 1 } // TODO: detect deeply nested indents in same line: // std::panic::set_hook(Box::new(move |info| { // hook(info); <-- indent here is 1 // })); // if last_scope && increment > 0 && ...{ ignore } node = parent; } assert!(increment >= 0); increment as usize } fn suggested_indent_for_line( syntax: Option<&Syntax>, text: RopeSlice, line_num: usize, tab_width: usize, ) -> usize { let line = text.line(line_num); let current = indent_level_for_line(line, tab_width); if let Some(start) = find_first_non_whitespace_char(text, line_num) { return suggested_indent_for_pos(syntax, text, start, false); }; // if the line is blank, indent should be zero 0 } // 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>, text: RopeSlice, pos: usize, new_line: bool, ) -> usize { if let Some(syntax) = syntax { let byte_start = text.char_to_byte(pos); let node = get_highest_syntax_node_at_bytepos(syntax, byte_start); // TODO: special case for comments // TODO: if preserve_leading_whitespace calculate_indentation(node, new_line) } else { // TODO: heuristics for non-tree sitter grammars 0 } } #[cfg(test)] mod test { use super::*; #[test] fn test_indent_level() { let tab_width = 4; let line = Rope::from(" fn new"); // 8 spaces assert_eq!(indent_level_for_line(line.slice(..), tab_width), 2); let line = Rope::from("\t\t\tfn new"); // 3 tabs assert_eq!(indent_level_for_line(line.slice(..), tab_width), 3); // mixed indentation let line = Rope::from("\t \tfn new"); // 1 tab, 4 spaces, tab assert_eq!(indent_level_for_line(line.slice(..), tab_width), 3); } #[test] fn test_suggested_indent_for_line() { let doc = Rope::from( " use std::{ io::{self, stdout, Stdout, Write}, path::PathBuf, sync::Arc, time::Duration, } mod test { fn hello_world() { 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] // match test { Some(a) => 1, None => { unimplemented!() } } std::panic::set_hook(Box::new(move |info| { hook(info); })); { { { 1 }}} pub fn change(document: &Document, changes: I) -> Self where I: IntoIterator + ExactSizeIterator, { true } ", ); let doc = Rope::from(doc); use crate::syntax::{ Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader, }; use once_cell::sync::OnceCell; let loader = Loader::new(Configuration { language: vec![LanguageConfiguration { scope: "source.rust".to_string(), file_types: vec!["rs".to_string()], language_id: Lang::Rust, highlight_config: OnceCell::new(), // path: "../helix-syntax/languages/tree-sitter-rust".into(), roots: vec![], language_server: None, indent: Some(IndentationConfiguration { tab_width: 4, unit: String::from(" "), }), }], }); let language_config = loader.language_config_for_scope("source.rust").unwrap(); let highlight_config = language_config.highlight_config(&[]).unwrap(); let syntax = Syntax::new(&doc, highlight_config.clone()); let text = doc.slice(..); let tab_width = 4; for i in 0..doc.len_lines() { let line = text.line(i); let indent = indent_level_for_line(line, tab_width); assert_eq!( suggested_indent_for_line(Some(&syntax), text, i, tab_width), indent, "line {}: {}", i, line ); } } }