From 938a710904ae6d328d4008626d98acb9e907813a Mon Sep 17 00:00:00 2001 From: Daniel Ebert Date: Tue, 19 Sep 2023 15:31:38 +0200 Subject: [PATCH] Make the indent heuristic configurable --- helix-core/src/indent.rs | 96 +++++++++++++++++++++----------------- helix-core/src/syntax.rs | 16 +++++++ helix-term/src/commands.rs | 3 ++ helix-view/src/editor.rs | 6 ++- 4 files changed, 76 insertions(+), 45 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index dfb33939d..26186ee83 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -6,7 +6,7 @@ use crate::{ chars::{char_is_line_ending, char_is_whitespace}, find_first_non_whitespace_char, graphemes::{grapheme_width, tab_width_at}, - syntax::{LanguageConfiguration, RopeProvider, Syntax}, + syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax}, tree_sitter::Node, Rope, RopeGraphemes, RopeSlice, }; @@ -931,6 +931,7 @@ pub fn treesitter_indent_for_pos<'a>( pub fn indent_for_newline( language_config: Option<&LanguageConfiguration>, syntax: Option<&Syntax>, + indent_heuristic: &IndentationHeuristic, indent_style: &IndentStyle, tab_width: usize, text: RopeSlice, @@ -939,7 +940,12 @@ pub fn indent_for_newline( current_line: usize, ) -> String { let indent_width = indent_style.indent_width(tab_width); - if let (Some(query), Some(syntax)) = ( + if let ( + IndentationHeuristic::TreeSitter | IndentationHeuristic::Hybrid, + Some(query), + Some(syntax), + ) = ( + indent_heuristic, language_config.and_then(|config| config.indent_query()), syntax, ) { @@ -953,49 +959,51 @@ pub fn indent_for_newline( line_before_end_pos, true, ) { - // We want to compute the indentation not only based on the - // syntax tree but also on the actual indentation of a previous - // line. This makes indentation computation more resilient to - // incomplete queries, incomplete source code & differing indentation - // styles for the same language. - // However, using the indent of a previous line as a baseline may not - // make sense, e.g. if it has a different alignment than the new line. - // In order to prevent edge cases with long running times, we only try - // a constant number of (non-empty) lines. - const MAX_ATTEMPTS: usize = 2; - let mut num_attempts = 0; - for line_idx in (0..=line_before).rev() { - let line = text.line(line_idx); - let first_non_whitespace_char = match find_first_non_whitespace_char(line) { - Some(i) => i, - None => { - continue; + if let IndentationHeuristic::Hybrid = indent_heuristic { + // We want to compute the indentation not only based on the + // syntax tree but also on the actual indentation of a previous + // line. This makes indentation computation more resilient to + // incomplete queries, incomplete source code & differing indentation + // styles for the same language. + // However, using the indent of a previous line as a baseline may not + // make sense, e.g. if it has a different alignment than the new line. + // In order to prevent edge cases with long running times, we only try + // a constant number of (non-empty) lines. + const MAX_ATTEMPTS: usize = 2; + let mut num_attempts = 0; + for line_idx in (0..=line_before).rev() { + let line = text.line(line_idx); + let first_non_whitespace_char = match find_first_non_whitespace_char(line) { + Some(i) => i, + None => { + continue; + } + }; + if let Some(indent) = (|| { + let computed_indent = treesitter_indent_for_pos( + query, + syntax, + tab_width, + indent_width, + text, + line_idx, + text.line_to_char(line_idx) + first_non_whitespace_char, + false, + )?; + let leading_whitespace = line.slice(0..first_non_whitespace_char); + indent.relative_indent( + &computed_indent, + leading_whitespace, + indent_style, + tab_width, + ) + })() { + return indent; + } + num_attempts += 1; + if num_attempts == MAX_ATTEMPTS { + break; } - }; - if let Some(indent) = (|| { - let computed_indent = treesitter_indent_for_pos( - query, - syntax, - tab_width, - indent_width, - text, - line_idx, - text.line_to_char(line_idx) + first_non_whitespace_char, - false, - )?; - let leading_whitespace = line.slice(0..first_non_whitespace_char); - indent.relative_indent( - &computed_indent, - leading_whitespace, - indent_style, - tab_width, - ) - })() { - return indent; - } - num_attempts += 1; - if num_attempts == MAX_ATTEMPTS { - break; } } return indent.to_string(indent_style, tab_width); diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 8c7fc4e15..dd9223160 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -442,6 +442,22 @@ pub struct IndentationConfiguration { pub unit: String, } +/// How the indentation for a newly inserted line should be determined. +/// If the selected heuristic is not available (e.g. because the current +/// language has no tree-sitter indent queries), a simpler one will be used. +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum IndentationHeuristic { + /// Just copy the indentation of the line that the cursor is currently on. + Simple, + /// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line. + TreeSitter, + /// Use tree-sitter indent queries to compute the expected difference in indentation between the new line + /// and the line before. Add this to the actual indentation level of the line before. + #[default] + Hybrid, +} + /// Configuration for auto pairs #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)] diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e557977ad..1b8f9e1f5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3053,6 +3053,7 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) { let indent = indent::indent_for_newline( language_config, syntax, + &doc.config.load().indent_heuristic, &doc.indent_style, tab_width, text, @@ -3181,6 +3182,7 @@ fn open(cx: &mut Context, open: Open) { let indent = indent::indent_for_newline( doc.language_config(), doc.syntax(), + &doc.config.load().indent_heuristic, &doc.indent_style, doc.tab_width(), text, @@ -3720,6 +3722,7 @@ pub mod insert { let indent = indent::indent_for_newline( doc.language_config(), doc.syntax(), + &doc.config.load().indent_heuristic, &doc.indent_style, doc.tab_width(), text, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5a81cb5d6..f2e853071 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -42,7 +42,7 @@ use anyhow::{anyhow, bail, Error}; pub use helix_core::diagnostic::Severity; use helix_core::{ auto_pairs::AutoPairs, - syntax::{self, AutoPairConfig, SoftWrap}, + syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap}, Change, LineEnding, NATIVE_LINE_ENDING, }; use helix_core::{Position, Selection}; @@ -291,6 +291,9 @@ pub struct Config { pub insert_final_newline: bool, /// Enables smart tab pub smart_tab: Option, + /// Which indent heuristic to use when a new line is inserted + #[serde(default)] + pub indent_heuristic: IndentationHeuristic, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] @@ -841,6 +844,7 @@ impl Default for Config { default_line_ending: LineEndingConfig::default(), insert_final_newline: true, smart_tab: Some(SmartTabConfig::default()), + indent_heuristic: IndentationHeuristic::default(), } } }