on master help sign in
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>

commit4da050b4bb639755e30447518aa79f7511c8952c authorTriton171 <daniel.ebert@protonmail.com> dateMon 3 Jan 2022, 03:03 (5 years ago) parent8f2af71
3 files changed+98−111
helix-core/src/indent.rs+62−78 helix-term/src/commands.rs+35−33 runtime/queries/rust/indents.toml+1
helix-core/src/indent.rs+62−78
@@ -1,6 +1,5 @@
11use crate::{
22 chars::{char_is_line_ending, char_is_whitespace},
3− find_first_non_whitespace_char,
43 syntax::{IndentQuery, LanguageConfiguration, Syntax},
54 tree_sitter::Node,
65 Rope, RopeSlice,
@@ -174,8 +173,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
174173
175174/// To determine indentation of a newly inserted line, figure out the indentation at the last col
176175/// of the previous line.
177−#[allow(dead_code)]
178−fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
176+pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
179177 let mut len = 0;
180178 for ch in line.chars() {
181179 match ch {
@@ -210,10 +208,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
210208 Some(node)
211209}
212210
213−fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
214− // NOTE: can't use contains() on query because of comparing Vec<String> and &str
215− // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
216−
211+/// Calculate the indentation at a given treesitter node.
212+/// If newline is false, then any "indent" nodes on the line are ignored ("outdent" still applies).
213+/// This is because the indentation is only increased starting at the second line of the node.
214+fn calculate_indentation(
215+ query: &IndentQuery,
216+ node: Option<Node>,
217+ line: usize,
218+ newline: bool,
219+) -> usize {
217220 let mut increment: isize = 0;
218221
219222 let mut node = match node {
@@ -221,70 +224,45 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
221224 None => return 0,
222225 };
223226
224− let mut prev_start = node.start_position().row;
225−
226− // if we're calculating indentation for a brand new line then the current node will become the
227− // parent node. We need to take it's indentation level into account too.
228− let node_kind = node.kind();
229− if newline && query.indent.contains(node_kind) {
230− increment += 1;
231− }
232−
233− while let Some(parent) = node.parent() {
234− let parent_kind = parent.kind();
235− let start = parent.start_position().row;
236−
237− // detect deeply nested indents in the same line
238− // .map(|a| { <-- ({ is two scopes
239− // let len = 1; <-- indents one level
240− // }) <-- }) is two scopes
241− let starts_same_line = start == prev_start;
242−
243− if query.outdent.contains(node.kind()) && !starts_same_line {
244− // we outdent by skipping the rules for the current level and jumping up
245− // node = parent;
246− increment -= 1;
247− // continue;
227+ let mut current_line = line;
228+ let mut consider_indent = newline;
229+ let mut increment_from_line: isize = 0;
230+
231+ loop {
232+ let node_kind = node.kind();
233+ let start = node.start_position().row;
234+ if current_line != start {
235+ // Indent/dedent by at most one per line:
236+ // .map(|a| { <-- ({ is two scopes
237+ // let len = 1; <-- indents one level
238+ // }) <-- }) is two scopes
239+ if consider_indent || increment_from_line < 0 {
240+ increment += increment_from_line.signum();
241+ }
242+ increment_from_line = 0;
243+ current_line = start;
244+ consider_indent = true;
248245 }
249246
250− if query.indent.contains(parent_kind) // && not_first_or_last_sibling
251− && !starts_same_line
252− {
253− // println!("is_scope {}", parent_kind);
254− prev_start = start;
255− increment += 1
247+ if query.outdent.contains(node_kind) {
248+ increment_from_line -= 1;
249+ }
250+ if query.indent.contains(node_kind) {
251+ increment_from_line += 1;
256252 }
257253
258− // if last_scope && increment > 0 && ...{ ignore }
259−
260− node = parent;
254+ if let Some(parent) = node.parent() {
255+ node = parent;
256+ } else {
257+ break;
258+ }
259+ }
260+ if consider_indent || increment_from_line < 0 {
261+ increment += increment_from_line.signum();
261262 }
262−
263263 increment.max(0) as usize
264264}
265265
266−#[allow(dead_code)]
267−fn suggested_indent_for_line(
268− language_config: &LanguageConfiguration,
269− syntax: Option<&Syntax>,
270− text: RopeSlice,
271− line_num: usize,
272− _tab_width: usize,
273−) -> usize {
274− if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
275− return suggested_indent_for_pos(
276− Some(language_config),
277− syntax,
278− text,
279− start + text.line_to_char(line_num),
280− false,
281− );
282− };
283−
284− // if the line is blank, indent should be zero
285− 0
286−}
287−
288266// TODO: two usecases: if we are triggering this for a new, blank line:
289267// - it should return 0 when mass indenting stuff
290268// - 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(
293271 syntax: Option<&Syntax>,
294272 text: RopeSlice,
295273 pos: usize,
274+ line: usize,
296275 new_line: bool,
297−) -> usize {
276+) -> Option<usize> {
298277 if let (Some(query), Some(syntax)) = (
299278 language_config.and_then(|config| config.indent_query()),
300279 syntax,
301280 ) {
302281 let byte_start = text.char_to_byte(pos);
303282 let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
304−
305− // let config = load indentation query config from Syntax(should contain language_config)
306−
307283 // TODO: special case for comments
308284 // TODO: if preserve_leading_whitespace
309− calculate_indentation(query, node, new_line)
285+ Some(calculate_indentation(query, node, line, new_line))
310286 } else {
311− // TODO: heuristics for non-tree sitter grammars
312− 0
287+ None
313288 }
314289}
315290
@@ -484,14 +459,23 @@ where
484459
485460 for i in 0..doc.len_lines() {
486461 let line = text.line(i);
487− let indent = indent_level_for_line(line, tab_width);
488− assert_eq!(
489− suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
490− indent,
491− "line {}: {}",
492− i,
493− line
494− );
462+ if let Some(pos) = crate::find_first_non_whitespace_char(line) {
463+ let indent = indent_level_for_line(line, tab_width);
464+ assert_eq!(
465+ suggested_indent_for_pos(
466+ Some(&language_config),
467+ Some(&syntax),
468+ text,
469+ text.line_to_char(i) + pos,
470+ i,
471+ false
472+ ),
473+ Some(indent),
474+ "line {}: \"{}\"",
475+ i,
476+ line
477+ );
478+ }
495479 }
496480 }
497481}
helix-term/src/commands.rs+35−33
@@ -3689,22 +3689,22 @@ fn open(cx: &mut Context, open: Open) {
36893689 let mut offs = 0;
36903690
36913691 let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
3692− let line = range.cursor_line(text);
3692+ let cursor_line = range.cursor_line(text);
36933693
3694− let line = match open {
3694+ let new_line = match open {
36953695 // adjust position to the end of the line (next line - 1)
3696− Open::Below => line + 1,
3696+ Open::Below => cursor_line + 1,
36973697 // adjust position to the end of the previous line (current line - 1)
3698− Open::Above => line,
3698+ Open::Above => cursor_line,
36993699 };
37003700
37013701 // Index to insert newlines after, as well as the char width
37023702 // to use to compensate for those inserted newlines.
3703− let (line_end_index, line_end_offset_width) = if line == 0 {
3703+ let (line_end_index, line_end_offset_width) = if new_line == 0 {
37043704 (0, 0)
37053705 } else {
37063706 (
3707− line_end_char_index(&doc.text().slice(..), line.saturating_sub(1)),
3707+ line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
37083708 doc.line_ending.len_chars(),
37093709 )
37103710 };
@@ -3715,8 +3715,10 @@ fn open(cx: &mut Context, open: Open) {
37153715 doc.syntax(),
37163716 text,
37173717 line_end_index,
3718+ new_line.saturating_sub(1),
37183719 true,
3719− );
3720+ )
3721+ .unwrap_or_else(|| indent::indent_level_for_line(text.line(cursor_line), doc.tab_width()));
37203722 let indent = doc.indent_unit().repeat(indent_level);
37213723 let indent_len = indent.len();
37223724 let mut text = String::with_capacity(1 + indent_len);
@@ -4451,48 +4453,48 @@ pub mod insert {
44514453 };
44524454 let curr = contents.get_char(pos).unwrap_or(' ');
44534455
4454− // TODO: offset range.head by 1? when calculating?
4456+ let current_line = text.char_to_line(pos);
44554457 let indent_level = indent::suggested_indent_for_pos(
44564458 doc.language_config(),
44574459 doc.syntax(),
44584460 text,
4459− pos.saturating_sub(1),
4461+ pos,
4462+ current_line,
44604463 true,
4461− );
4462− let indent = doc.indent_unit().repeat(indent_level);
4463− let mut text = String::with_capacity(1 + indent.len());
4464− text.push_str(doc.line_ending.as_str());
4465− text.push_str(&indent);
4464+ )
4465+ .unwrap_or_else(|| {
4466+ indent::indent_level_for_line(text.line(current_line), doc.tab_width())
4467+ });
44664468
4467− let head = pos + offs + text.chars().count();
4469+ let indent = doc.indent_unit().repeat(indent_level);
4470+ let mut text = String::new();
4471+ // 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
4472+ let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
4473+ let inner_indent = doc.indent_unit().repeat(indent_level + 1);
4474+ text.reserve_exact(2 + indent.len() + inner_indent.len());
4475+ text.push_str(doc.line_ending.as_str());
4476+ text.push_str(&inner_indent);
4477+ let new_head_pos = pos + offs + text.chars().count();
4478+ text.push_str(doc.line_ending.as_str());
4479+ text.push_str(&indent);
4480+ new_head_pos
4481+ } else {
4482+ text.reserve_exact(1 + indent.len());
4483+ text.push_str(doc.line_ending.as_str());
4484+ text.push_str(&indent);
4485+ pos + offs + text.chars().count()
4486+ };
44684487
44694488 // TODO: range replace or extend
44704489 // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
44714490 // can be used with cx.mode to do replace or extend on most changes
4472− ranges.push(Range::new(
4473− if range.is_empty() {
4474− head
4475− } else {
4476− range.anchor + offs
4477− },
4478− head,
4479− ));
4480−
4481− // if between a bracket pair
4482− if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
4483− // another newline, indent the end bracket one level less
4484− let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
4485− text.push_str(doc.line_ending.as_str());
4486− text.push_str(&indent);
4487− }
4488−
4491+ ranges.push(Range::new(new_head_pos, new_head_pos));
44894492 offs += text.chars().count();
44904493
44914494 (pos, pos, Some(text.into()))
44924495 });
44934496
44944497 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
4495− //
44964498
44974499 doc.apply(&transaction, view.id);
44984500 }
runtime/queries/rust/indents.toml+1
@@ -9,6 +9,7 @@ indent = [
99 "field_initializer_list",
1010 "struct_pattern",
1111 "tuple_pattern",
12+ "unit_expression",
1213 "enum_variant_list",
1314 "call_expression",
1415 "binary_expression",