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)
4da050bTriton171
helix-core/src/indent.rs+62−78
@@ -1,6 +1,5 @@
1use crate::{
1use crate::{
2 chars::{char_is_line_ending, char_is_whitespace},
2 chars::{char_is_line_ending, char_is_whitespace},
3− find_first_non_whitespace_char,
4 syntax::{IndentQuery, LanguageConfiguration, Syntax},
3 syntax::{IndentQuery, LanguageConfiguration, Syntax},
5 tree_sitter::Node,
4 tree_sitter::Node,
6 Rope, RopeSlice,
5 Rope, RopeSlice,
@@ -174,8 +173,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
174
173
175/// To determine indentation of a newly inserted line, figure out the indentation at the last col
174/// To determine indentation of a newly inserted line, figure out the indentation at the last col
176/// of the previous line.
175/// 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 {
179 let mut len = 0;
177 let mut len = 0;
180 for ch in line.chars() {
178 for ch in line.chars() {
181 match ch {
179 match ch {
@@ -210,10 +208,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
210 Some(node)
208 Some(node)
211}
209}
212
210
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.
213−fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
214+fn calculate_indentation(
214− // NOTE: can't use contains() on query because of comparing Vec<String> and &str
215+ query: &IndentQuery,
215− // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
216+ node: Option<Node>,
216−
217+ line: usize,
218+ newline: bool,
219+) -> usize {
217 let mut increment: isize = 0;
220 let mut increment: isize = 0;
218
221
219 let mut node = match node {
222 let mut node = match node {
@@ -221,70 +224,45 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
221 None => return 0,
224 None => return 0,
222 };
225 };
223
226
224− let mut prev_start = node.start_position().row;
227+ let mut current_line = line;
225−
228+ let mut consider_indent = newline;
226− // if we're calculating indentation for a brand new line then the current node will become the
229+ let mut increment_from_line: isize = 0;
227− // parent node. We need to take it's indentation level into account too.
230+
228− let node_kind = node.kind();
231+ loop {
229− if newline && query.indent.contains(node_kind) {
232+ let node_kind = 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;
233+ let start = node.start_position().row;
236−
234+ if current_line != start {
237− // detect deeply nested indents in the same line
235+ // Indent/dedent by at most one per line:
238− // .map(|a| { <-- ({ is two scopes
236+ // .map(|a| { <-- ({ is two scopes
239− // let len = 1; <-- indents one level
237+ // let len = 1; <-- indents one level
240− // }) <-- }) is two scopes
238+ // }) <-- }) is two scopes
241− let starts_same_line = start == prev_start;
239+ if consider_indent || increment_from_line < 0 {
242−
240+ increment += increment_from_line.signum();
243− if query.outdent.contains(node.kind()) && !starts_same_line {
241+ }
244− // we outdent by skipping the rules for the current level and jumping up
242+ increment_from_line = 0;
245− // node = parent;
243+ current_line = start;
246− increment -= 1;
244+ consider_indent = true;
247− // continue;
248 }
245 }
249
246
250− if query.indent.contains(parent_kind) // && not_first_or_last_sibling
247+ if query.outdent.contains(node_kind) {
251− && !starts_same_line
248+ increment_from_line -= 1;
252− {
249+ }
253− // println!("is_scope {}", parent_kind);
250+ if query.indent.contains(node_kind) {
254− prev_start = start;
251+ increment_from_line += 1;
255− increment += 1
256 }
252 }
257
253
258− // if last_scope && increment > 0 && ...{ ignore }
254+ if let Some(parent) = node.parent() {
259−
255+ node = parent;
260− node = parent;
256+ } else {
257+ break;
258+ }
259+ }
260+ if consider_indent || increment_from_line < 0 {
261+ increment += increment_from_line.signum();
261 }
262 }
262−
263 increment.max(0) as usize
263 increment.max(0) as usize
264}
264}
265
265
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−
288// TODO: two usecases: if we are triggering this for a new, blank line:
266// TODO: two usecases: if we are triggering this for a new, blank line:
289// - it should return 0 when mass indenting stuff
267// - it should return 0 when mass indenting stuff
290// - it should look up the wrapper node and count it too when we press o/O
268// - 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(
293 syntax: Option<&Syntax>,
271 syntax: Option<&Syntax>,
294 text: RopeSlice,
272 text: RopeSlice,
295 pos: usize,
273 pos: usize,
274+ line: usize,
296 new_line: bool,
275 new_line: bool,
297−) -> usize {
276+) -> Option<usize> {
298 if let (Some(query), Some(syntax)) = (
277 if let (Some(query), Some(syntax)) = (
299 language_config.and_then(|config| config.indent_query()),
278 language_config.and_then(|config| config.indent_query()),
300 syntax,
279 syntax,
301 ) {
280 ) {
302 let byte_start = text.char_to_byte(pos);
281 let byte_start = text.char_to_byte(pos);
303 let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
282 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−
307 // TODO: special case for comments
283 // TODO: special case for comments
308 // TODO: if preserve_leading_whitespace
284 // TODO: if preserve_leading_whitespace
309− calculate_indentation(query, node, new_line)
285+ Some(calculate_indentation(query, node, line, new_line))
310 } else {
286 } else {
311− // TODO: heuristics for non-tree sitter grammars
287+ None
312− 0
313 }
288 }
314}
289}
315
290
@@ -484,14 +459,23 @@ where
484
459
485 for i in 0..doc.len_lines() {
460 for i in 0..doc.len_lines() {
486 let line = text.line(i);
461 let line = text.line(i);
462+ if let Some(pos) = crate::find_first_non_whitespace_char(line) {
487− let indent = indent_level_for_line(line, tab_width);
463+ let indent = indent_level_for_line(line, tab_width);
488− assert_eq!(
464+ assert_eq!(
489− suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
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+ ),
490− indent,
473+ Some(indent),
491− "line {}: {}",
474+ "line {}: \"{}\"",
492− i,
475+ i,
493− line
476+ line
494− );
477+ );
478+ }
495 }
479 }
496 }
480 }
497}
481}
helix-term/src/commands.rs+35−33
@@ -3689,22 +3689,22 @@ fn open(cx: &mut Context, open: Open) {
3689 let mut offs = 0;
3689 let mut offs = 0;
3690
3690
3691 let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
3691 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);
3693
3693
3694− let line = match open {
3694+ let new_line = match open {
3695 // adjust position to the end of the line (next line - 1)
3695 // adjust position to the end of the line (next line - 1)
3696− Open::Below => line + 1,
3696+ Open::Below => cursor_line + 1,
3697 // adjust position to the end of the previous line (current line - 1)
3697 // adjust position to the end of the previous line (current line - 1)
3698− Open::Above => line,
3698+ Open::Above => cursor_line,
3699 };
3699 };
3700
3700
3701 // Index to insert newlines after, as well as the char width
3701 // Index to insert newlines after, as well as the char width
3702 // to use to compensate for those inserted newlines.
3702 // 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 {
3704 (0, 0)
3704 (0, 0)
3705 } else {
3705 } else {
3706 (
3706 (
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)),
3708 doc.line_ending.len_chars(),
3708 doc.line_ending.len_chars(),
3709 )
3709 )
3710 };
3710 };
@@ -3715,8 +3715,10 @@ fn open(cx: &mut Context, open: Open) {
3715 doc.syntax(),
3715 doc.syntax(),
3716 text,
3716 text,
3717 line_end_index,
3717 line_end_index,
3718+ new_line.saturating_sub(1),
3718 true,
3719 true,
3719− );
3720+ )
3721+ .unwrap_or_else(|| indent::indent_level_for_line(text.line(cursor_line), doc.tab_width()));
3720 let indent = doc.indent_unit().repeat(indent_level);
3722 let indent = doc.indent_unit().repeat(indent_level);
3721 let indent_len = indent.len();
3723 let indent_len = indent.len();
3722 let mut text = String::with_capacity(1 + indent_len);
3724 let mut text = String::with_capacity(1 + indent_len);
@@ -4451,48 +4453,48 @@ pub mod insert {
4451 };
4453 };
4452 let curr = contents.get_char(pos).unwrap_or(' ');
4454 let curr = contents.get_char(pos).unwrap_or(' ');
4453
4455
4454− // TODO: offset range.head by 1? when calculating?
4456+ let current_line = text.char_to_line(pos);
4455 let indent_level = indent::suggested_indent_for_pos(
4457 let indent_level = indent::suggested_indent_for_pos(
4456 doc.language_config(),
4458 doc.language_config(),
4457 doc.syntax(),
4459 doc.syntax(),
4458 text,
4460 text,
4459− pos.saturating_sub(1),
4461+ pos,
4462+ current_line,
4460 true,
4463 true,
4461− );
4464+ )
4462− let indent = doc.indent_unit().repeat(indent_level);
4465+ .unwrap_or_else(|| {
4463− let mut text = String::with_capacity(1 + indent.len());
4466+ indent::indent_level_for_line(text.line(current_line), doc.tab_width())
4464− text.push_str(doc.line_ending.as_str());
4467+ });
4465− text.push_str(&indent);
4466
4468
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);
4467− let head = pos + offs + text.chars().count();
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+ };
4468
4487
4469 // TODO: range replace or extend
4488 // TODO: range replace or extend
4470 // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
4489 // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
4471 // can be used with cx.mode to do replace or extend on most changes
4490 // can be used with cx.mode to do replace or extend on most changes
4472− ranges.push(Range::new(
4491+ ranges.push(Range::new(new_head_pos, new_head_pos));
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−
4489 offs += text.chars().count();
4492 offs += text.chars().count();
4490
4493
4491 (pos, pos, Some(text.into()))
4494 (pos, pos, Some(text.into()))
4492 });
4495 });
4493
4496
4494 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
4497 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
4495− //
4496
4498
4497 doc.apply(&transaction, view.id);
4499 doc.apply(&transaction, view.id);
4498 }
4500 }
runtime/queries/rust/indents.toml+1
@@ -9,6 +9,7 @@ indent = [
9 "field_initializer_list",
9 "field_initializer_list",
10 "struct_pattern",
10 "struct_pattern",
11 "tuple_pattern",
11 "tuple_pattern",
12+ "unit_expression",
12 "enum_variant_list",
13 "enum_variant_list",
13 "call_expression",
14 "call_expression",
14 "binary_expression",
15 "binary_expression",