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 <triton0171@gmail.com>
pull/1293/head
Triton171 3 years ago committed by GitHub
parent 8f2af71340
commit 4da050b4bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,5 @@
use crate::{ use crate::{
chars::{char_is_line_ending, char_is_whitespace}, chars::{char_is_line_ending, char_is_whitespace},
find_first_non_whitespace_char,
syntax::{IndentQuery, LanguageConfiguration, Syntax}, syntax::{IndentQuery, LanguageConfiguration, Syntax},
tree_sitter::Node, tree_sitter::Node,
Rope, RopeSlice, Rope, RopeSlice,
@ -174,8 +173,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
/// To determine indentation of a newly inserted line, figure out the indentation at the last col /// To determine indentation of a newly inserted line, figure out the indentation at the last col
/// of the previous line. /// of the previous line.
#[allow(dead_code)] pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
let mut len = 0; let mut len = 0;
for ch in line.chars() { for ch in line.chars() {
match ch { match ch {
@ -210,10 +208,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
Some(node) Some(node)
} }
fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize { /// Calculate the indentation at a given treesitter node.
// NOTE: can't use contains() on query because of comparing Vec<String> and &str /// If newline is false, then any "indent" nodes on the line are ignored ("outdent" still applies).
// https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains /// This is because the indentation is only increased starting at the second line of the node.
fn calculate_indentation(
query: &IndentQuery,
node: Option<Node>,
line: usize,
newline: bool,
) -> usize {
let mut increment: isize = 0; let mut increment: isize = 0;
let mut node = match node { let mut node = match node {
@ -221,68 +224,43 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
None => return 0, None => return 0,
}; };
let mut prev_start = node.start_position().row; let mut current_line = line;
let mut consider_indent = newline;
let mut increment_from_line: isize = 0;
// if we're calculating indentation for a brand new line then the current node will become the loop {
// parent node. We need to take it's indentation level into account too.
let node_kind = node.kind(); let node_kind = node.kind();
if newline && query.indent.contains(node_kind) { let start = node.start_position().row;
increment += 1; if current_line != start {
} // Indent/dedent by at most one per line:
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 // .map(|a| { <-- ({ is two scopes
// let len = 1; <-- indents one level // let len = 1; <-- indents one level
// }) <-- }) is two scopes // }) <-- }) is two scopes
let starts_same_line = start == prev_start; if consider_indent || increment_from_line < 0 {
increment += increment_from_line.signum();
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;
} }
increment_from_line = 0;
if query.indent.contains(parent_kind) // && not_first_or_last_sibling current_line = start;
&& !starts_same_line consider_indent = true;
{
// println!("is_scope {}", parent_kind);
prev_start = start;
increment += 1
} }
// if last_scope && increment > 0 && ...{ ignore } if query.outdent.contains(node_kind) {
increment_from_line -= 1;
}
if query.indent.contains(node_kind) {
increment_from_line += 1;
}
if let Some(parent) = node.parent() {
node = parent; node = parent;
} else {
break;
} }
increment.max(0) as usize
} }
if consider_indent || increment_from_line < 0 {
#[allow(dead_code)] increment += increment_from_line.signum();
fn suggested_indent_for_line( }
language_config: &LanguageConfiguration, increment.max(0) as usize
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: // TODO: two usecases: if we are triggering this for a new, blank line:
@ -293,23 +271,20 @@ pub fn suggested_indent_for_pos(
syntax: Option<&Syntax>, syntax: Option<&Syntax>,
text: RopeSlice, text: RopeSlice,
pos: usize, pos: usize,
line: usize,
new_line: bool, new_line: bool,
) -> usize { ) -> Option<usize> {
if let (Some(query), Some(syntax)) = ( if let (Some(query), Some(syntax)) = (
language_config.and_then(|config| config.indent_query()), language_config.and_then(|config| config.indent_query()),
syntax, syntax,
) { ) {
let byte_start = text.char_to_byte(pos); let byte_start = text.char_to_byte(pos);
let node = get_highest_syntax_node_at_bytepos(syntax, byte_start); 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: special case for comments
// TODO: if preserve_leading_whitespace // TODO: if preserve_leading_whitespace
calculate_indentation(query, node, new_line) Some(calculate_indentation(query, node, line, new_line))
} else { } else {
// TODO: heuristics for non-tree sitter grammars None
0
} }
} }
@ -484,14 +459,23 @@ where
for i in 0..doc.len_lines() { for i in 0..doc.len_lines() {
let line = text.line(i); let line = text.line(i);
if let Some(pos) = crate::find_first_non_whitespace_char(line) {
let indent = indent_level_for_line(line, tab_width); let indent = indent_level_for_line(line, tab_width);
assert_eq!( assert_eq!(
suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width), suggested_indent_for_pos(
indent, Some(&language_config),
"line {}: {}", Some(&syntax),
text,
text.line_to_char(i) + pos,
i,
false
),
Some(indent),
"line {}: \"{}\"",
i, i,
line line
); );
} }
} }
} }
}

@ -3689,22 +3689,22 @@ fn open(cx: &mut Context, open: Open) {
let mut offs = 0; let mut offs = 0;
let mut transaction = Transaction::change_by_selection(contents, selection, |range| { 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) // 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) // 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 // Index to insert newlines after, as well as the char width
// to use to compensate for those inserted newlines. // 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) (0, 0)
} else { } 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(), doc.line_ending.len_chars(),
) )
}; };
@ -3715,8 +3715,10 @@ fn open(cx: &mut Context, open: Open) {
doc.syntax(), doc.syntax(),
text, text,
line_end_index, line_end_index,
new_line.saturating_sub(1),
true, 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 = doc.indent_unit().repeat(indent_level);
let indent_len = indent.len(); let indent_len = indent.len();
let mut text = String::with_capacity(1 + 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(' '); 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( let indent_level = indent::suggested_indent_for_pos(
doc.language_config(), doc.language_config(),
doc.syntax(), doc.syntax(),
text, text,
pos.saturating_sub(1), pos,
current_line,
true, true,
); )
.unwrap_or_else(|| {
indent::indent_level_for_line(text.line(current_line), doc.tab_width())
});
let indent = doc.indent_unit().repeat(indent_level); let indent = doc.indent_unit().repeat(indent_level);
let mut text = String::with_capacity(1 + indent.len()); 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(doc.line_ending.as_str());
text.push_str(&indent); text.push_str(&indent);
new_head_pos
let head = 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 { } else {
range.anchor + offs text.reserve_exact(1 + indent.len());
},
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(doc.line_ending.as_str());
text.push_str(&indent); 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(new_head_pos, new_head_pos));
offs += text.chars().count(); offs += text.chars().count();
(pos, pos, Some(text.into())) (pos, pos, Some(text.into()))
}); });
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
//
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
} }

@ -9,6 +9,7 @@ indent = [
"field_initializer_list", "field_initializer_list",
"struct_pattern", "struct_pattern",
"tuple_pattern", "tuple_pattern",
"unit_expression",
"enum_variant_list", "enum_variant_list",
"call_expression", "call_expression",
"binary_expression", "binary_expression",

Loading…
Cancel
Save