From 4da050b4bb639755e30447518aa79f7511c8952c Mon Sep 17 00:00:00 2001 From: Triton171 Date: Mon, 3 Jan 2022 03:03:57 +0100 Subject: [PATCH] Add basic indentation for languages without treesitter-based indentation rules (always use the indent of the current line for a new line). (#1341) Fix several bugs in the treesitter indentation calculation. Co-authored-by: Triton171 --- helix-core/src/indent.rs | 140 +++++++++++++----------------- helix-term/src/commands.rs | 68 ++++++++------- runtime/queries/rust/indents.toml | 1 + 3 files changed, 98 insertions(+), 111 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index c2baf3cc..28066aa6 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,6 +1,5 @@ use crate::{ chars::{char_is_line_ending, char_is_whitespace}, - find_first_non_whitespace_char, syntax::{IndentQuery, LanguageConfiguration, Syntax}, tree_sitter::Node, Rope, RopeSlice, @@ -174,8 +173,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option { /// To determine indentation of a newly inserted line, figure out the indentation at the last col /// of the previous line. -#[allow(dead_code)] -fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { +pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { let mut len = 0; for ch in line.chars() { match ch { @@ -210,10 +208,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option, newline: bool) -> usize { - // NOTE: can't use contains() on query because of comparing Vec and &str - // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains - +/// Calculate the indentation at a given treesitter node. +/// If newline is false, then any "indent" nodes on the line are ignored ("outdent" still applies). +/// This is because the indentation is only increased starting at the second line of the node. +fn calculate_indentation( + query: &IndentQuery, + node: Option, + line: usize, + newline: bool, +) -> usize { let mut increment: isize = 0; let mut node = match node { @@ -221,70 +224,45 @@ fn calculate_indentation(query: &IndentQuery, node: Option, newline: bool) 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 && query.indent.contains(node_kind) { - increment += 1; - } - - while let Some(parent) = node.parent() { - let parent_kind = parent.kind(); - let start = parent.start_position().row; - - // detect deeply nested indents in the same line - // .map(|a| { <-- ({ is two scopes - // let len = 1; <-- indents one level - // }) <-- }) is two scopes - let starts_same_line = start == prev_start; - - if query.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; + let mut current_line = line; + let mut consider_indent = newline; + let mut increment_from_line: isize = 0; + + loop { + let node_kind = node.kind(); + let start = node.start_position().row; + if current_line != start { + // Indent/dedent by at most one per line: + // .map(|a| { <-- ({ is two scopes + // let len = 1; <-- indents one level + // }) <-- }) is two scopes + if consider_indent || increment_from_line < 0 { + increment += increment_from_line.signum(); + } + increment_from_line = 0; + current_line = start; + consider_indent = true; } - if query.indent.contains(parent_kind) // && not_first_or_last_sibling - && !starts_same_line - { - // println!("is_scope {}", parent_kind); - prev_start = start; - increment += 1 + if query.outdent.contains(node_kind) { + increment_from_line -= 1; + } + if query.indent.contains(node_kind) { + increment_from_line += 1; } - // if last_scope && increment > 0 && ...{ ignore } - - node = parent; + if let Some(parent) = node.parent() { + node = parent; + } else { + break; + } + } + if consider_indent || increment_from_line < 0 { + increment += increment_from_line.signum(); } - increment.max(0) as usize } -#[allow(dead_code)] -fn suggested_indent_for_line( - language_config: &LanguageConfiguration, - syntax: Option<&Syntax>, - text: RopeSlice, - line_num: usize, - _tab_width: usize, -) -> usize { - if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) { - return suggested_indent_for_pos( - Some(language_config), - syntax, - text, - start + text.line_to_char(line_num), - 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 @@ -293,23 +271,20 @@ pub fn suggested_indent_for_pos( syntax: Option<&Syntax>, text: RopeSlice, pos: usize, + line: usize, new_line: bool, -) -> usize { +) -> Option { if let (Some(query), Some(syntax)) = ( language_config.and_then(|config| config.indent_query()), syntax, ) { let byte_start = text.char_to_byte(pos); let node = get_highest_syntax_node_at_bytepos(syntax, byte_start); - - // let config = load indentation query config from Syntax(should contain language_config) - // TODO: special case for comments // TODO: if preserve_leading_whitespace - calculate_indentation(query, node, new_line) + Some(calculate_indentation(query, node, line, new_line)) } else { - // TODO: heuristics for non-tree sitter grammars - 0 + None } } @@ -484,14 +459,23 @@ where 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(&language_config, Some(&syntax), text, i, tab_width), - indent, - "line {}: {}", - i, - line - ); + if let Some(pos) = crate::find_first_non_whitespace_char(line) { + let indent = indent_level_for_line(line, tab_width); + assert_eq!( + suggested_indent_for_pos( + Some(&language_config), + Some(&syntax), + text, + text.line_to_char(i) + pos, + i, + false + ), + Some(indent), + "line {}: \"{}\"", + i, + line + ); + } } } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e61c3cf3..842d8b60 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3689,22 +3689,22 @@ fn open(cx: &mut Context, open: Open) { let mut offs = 0; let mut transaction = Transaction::change_by_selection(contents, selection, |range| { - let line = range.cursor_line(text); + let cursor_line = range.cursor_line(text); - let line = match open { + let new_line = match open { // adjust position to the end of the line (next line - 1) - Open::Below => line + 1, + Open::Below => cursor_line + 1, // adjust position to the end of the previous line (current line - 1) - Open::Above => line, + Open::Above => cursor_line, }; // Index to insert newlines after, as well as the char width // to use to compensate for those inserted newlines. - let (line_end_index, line_end_offset_width) = if line == 0 { + let (line_end_index, line_end_offset_width) = if new_line == 0 { (0, 0) } else { ( - line_end_char_index(&doc.text().slice(..), line.saturating_sub(1)), + line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)), doc.line_ending.len_chars(), ) }; @@ -3715,8 +3715,10 @@ fn open(cx: &mut Context, open: Open) { doc.syntax(), text, line_end_index, + new_line.saturating_sub(1), true, - ); + ) + .unwrap_or_else(|| indent::indent_level_for_line(text.line(cursor_line), doc.tab_width())); let indent = doc.indent_unit().repeat(indent_level); let indent_len = indent.len(); let mut text = String::with_capacity(1 + indent_len); @@ -4451,48 +4453,48 @@ pub mod insert { }; let curr = contents.get_char(pos).unwrap_or(' '); - // TODO: offset range.head by 1? when calculating? + let current_line = text.char_to_line(pos); let indent_level = indent::suggested_indent_for_pos( doc.language_config(), doc.syntax(), text, - pos.saturating_sub(1), + pos, + current_line, true, - ); - let indent = doc.indent_unit().repeat(indent_level); - let mut text = String::with_capacity(1 + indent.len()); - text.push_str(doc.line_ending.as_str()); - text.push_str(&indent); + ) + .unwrap_or_else(|| { + indent::indent_level_for_line(text.line(current_line), doc.tab_width()) + }); - let head = pos + offs + text.chars().count(); + let indent = doc.indent_unit().repeat(indent_level); + let mut text = String::new(); + // If we are between pairs (such as brackets), we want to insert an additional line which is indented one level more and place the cursor there + let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) { + let inner_indent = doc.indent_unit().repeat(indent_level + 1); + text.reserve_exact(2 + indent.len() + inner_indent.len()); + text.push_str(doc.line_ending.as_str()); + text.push_str(&inner_indent); + let new_head_pos = pos + offs + text.chars().count(); + text.push_str(doc.line_ending.as_str()); + text.push_str(&indent); + new_head_pos + } else { + text.reserve_exact(1 + indent.len()); + text.push_str(doc.line_ending.as_str()); + text.push_str(&indent); + pos + offs + text.chars().count() + }; // TODO: range replace or extend // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos // can be used with cx.mode to do replace or extend on most changes - ranges.push(Range::new( - if range.is_empty() { - head - } else { - range.anchor + offs - }, - head, - )); - - // if between a bracket pair - if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) { - // another newline, indent the end bracket one level less - let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1)); - text.push_str(doc.line_ending.as_str()); - text.push_str(&indent); - } - + ranges.push(Range::new(new_head_pos, new_head_pos)); offs += text.chars().count(); (pos, pos, Some(text.into())) }); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); - // doc.apply(&transaction, view.id); } diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml index 3900f0b9..51a0ceea 100644 --- a/runtime/queries/rust/indents.toml +++ b/runtime/queries/rust/indents.toml @@ -9,6 +9,7 @@ indent = [ "field_initializer_list", "struct_pattern", "tuple_pattern", + "unit_expression", "enum_variant_list", "call_expression", "binary_expression",