mirror of
https://github.com/ilyakooo0/helix.git
synced 2024-09-11 16:07:00 +03:00
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>
This commit is contained in:
parent
8f2af71340
commit
4da050b4bb
@ -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<IndentStyle> {
|
||||
|
||||
/// 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<Nod
|
||||
Some(node)
|
||||
}
|
||||
|
||||
fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
|
||||
// NOTE: can't use contains() on query because of comparing Vec<String> 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<Node>,
|
||||
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<Node>, newline: bool)
|
||||
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
|
||||
// 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;
|
||||
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<usize> {
|
||||
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 @@ pub fn change<I>(document: &Document, changes: I) -> Self
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 fn insert_newline(cx: &mut Context) {
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ indent = [
|
||||
"field_initializer_list",
|
||||
"struct_pattern",
|
||||
"tuple_pattern",
|
||||
"unit_expression",
|
||||
"enum_variant_list",
|
||||
"call_expression",
|
||||
"binary_expression",
|
||||
|
Loading…
Reference in New Issue
Block a user