Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/indent.rs')
-rw-r--r--helix-core/src/indent.rs599
1 files changed, 294 insertions, 305 deletions
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index bdf0f405..2a5f5d93 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -1,20 +1,36 @@
use std::{borrow::Cow, collections::HashMap};
-use helix_stdx::rope::RopeSliceExt;
-use tree_house::TREE_SITTER_MATCH_LIMIT;
+use anyhow::{anyhow, bail};
+use helix_config::{config_serde_adapter, options, IntegerRangeValidator};
+use serde::{Deserialize, Serialize};
+use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
+ find_first_non_whitespace_char,
graphemes::{grapheme_width, tab_width_at},
- syntax::{self, config::IndentationHeuristic},
- tree_sitter::{
- self,
- query::{InvalidPredicateError, UserPredicate},
- Capture, Grammar, InactiveQueryCursor, Node, Pattern, Query, QueryMatch, RopeInput,
- },
- Position, Rope, RopeSlice, Syntax, Tendril,
+ syntax::{LanguageConfiguration, RopeProvider, Syntax},
+ tree_sitter::Node,
+ Position, Rope, RopeGraphemes, RopeSlice,
};
+/// 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, Copy, 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,
+}
+config_serde_adapter!(IndentationHeuristic);
+
/// Enum representing indentation style.
///
/// Only values 1-8 are valid for the `Spaces` variant.
@@ -24,6 +40,50 @@ pub enum IndentStyle {
Spaces(u8),
}
+options! {
+ struct IndentationConfig {
+ /// The number columns that a tabs are aligned to.
+ #[name = "ident.tab_width"]
+ #[read = copy]
+ tab_width: usize = 4,
+ /// Indentation inserted/removed into the document when indenting/dedenting.
+ /// This can be set to an integer representing N spaces or "tab" for tabs.
+ #[name = "ident.unit"]
+ #[read = copy]
+ indent_style: IndentStyle = IndentStyle::Tabs,
+ /// How the indentation for a newly inserted line is computed:
+ /// `simple` just copies the indentation level from the previous line,
+ /// `tree-sitter` computes the indentation based on the syntax tree and
+ /// `hybrid` combines both approaches.
+ /// If the chosen heuristic is not available, a different one will
+ /// be used as a fallback (the fallback order being `hybrid` ->
+ /// `tree-sitter` -> `simple`).
+ #[read = copy]
+ indent_heuristic: IndentationHeuristic = IndentationHeuristic::Hybrid
+ }
+}
+
+impl helix_config::Ty for IndentStyle {
+ fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
+ match val {
+ helix_config::Value::String(s) if s == "t" || s == "tab" => Ok(IndentStyle::Tabs),
+ helix_config::Value::Int(_) => {
+ let spaces = IntegerRangeValidator::new(0, MAX_INDENT)
+ .validate(val)
+ .map_err(|err| anyhow!("invalid number of spaces! {err}"))?;
+ Ok(IndentStyle::Spaces(spaces))
+ }
+ _ => bail!("expected an integer (spaces) or 'tab'"),
+ }
+ }
+ fn to_value(&self) -> helix_config::Value {
+ match *self {
+ IndentStyle::Tabs => helix_config::Value::String("tab".into()),
+ IndentStyle::Spaces(spaces) => helix_config::Value::Int(spaces as _),
+ }
+ }
+}
+
// 16 spaces
const INDENTS: &str = " ";
pub const MAX_INDENT: u8 = 16;
@@ -153,12 +213,6 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
// Give more weight to tabs, because their presence is a very
// strong indicator.
histogram[0] *= 2;
- // Gives less weight to single indent, as single spaces are
- // often used in certain languages' comment systems and rarely
- // used as the actual document indentation.
- if histogram[1] > 1 {
- histogram[1] /= 2;
- }
histogram
};
@@ -210,49 +264,16 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: us
/// Create a string of tabs & spaces that has the same visual width as the given RopeSlice (independent of the tab width).
fn whitespace_with_same_width(text: RopeSlice) -> String {
let mut s = String::new();
- for grapheme in text.graphemes() {
+ for grapheme in RopeGraphemes::new(text) {
if grapheme == "\t" {
s.push('\t');
} else {
- s.extend(std::iter::repeat_n(
- ' ',
- grapheme_width(&Cow::from(grapheme)),
- ));
+ s.extend(std::iter::repeat(' ').take(grapheme_width(&Cow::from(grapheme))));
}
}
s
}
-/// normalizes indentation to tabs/spaces based on user configuration
-/// This function does not change the actual indentation width, just the character
-/// composition.
-pub fn normalize_indentation(
- prefix: RopeSlice<'_>,
- line: RopeSlice<'_>,
- dst: &mut Tendril,
- indent_style: IndentStyle,
- tab_width: usize,
-) -> usize {
- #[allow(deprecated)]
- let off = crate::visual_coords_at_pos(prefix, prefix.len_chars(), tab_width).col;
- let mut len = 0;
- let mut original_len = 0;
- for ch in line.chars() {
- match ch {
- '\t' => len += tab_width_at(len + off, tab_width as u16),
- ' ' => len += 1,
- _ => break,
- }
- original_len += 1;
- }
- if indent_style == IndentStyle::Tabs {
- dst.extend(std::iter::repeat_n('\t', len / tab_width));
- len %= tab_width;
- }
- dst.extend(std::iter::repeat_n(' ', len));
- original_len
-}
-
fn add_indent_level(
mut base_indent: String,
added_indent_level: isize,
@@ -290,171 +311,48 @@ fn add_indent_level(
}
}
-/// Return true if only whitespace comes before the node on its line.
-/// If given, new_line_byte_pos is treated the same way as any existing newline.
-fn is_first_in_line(node: &Node, text: RopeSlice, new_line_byte_pos: Option<u32>) -> bool {
- let line = text.byte_to_line(node.start_byte() as usize);
- let mut line_start_byte_pos = text.line_to_byte(line) as u32;
- if let Some(pos) = new_line_byte_pos {
- if line_start_byte_pos < pos && pos <= node.start_byte() {
- line_start_byte_pos = pos;
- }
- }
- text.byte_slice(line_start_byte_pos as usize..node.start_byte() as usize)
- .chars()
- .all(|c| c.is_whitespace())
-}
-
-#[derive(Debug, Default)]
-pub struct IndentQueryPredicates {
- not_kind_eq: Vec<(Capture, Box<str>)>,
- same_line: Option<(Capture, Capture, bool)>,
- one_line: Option<(Capture, bool)>,
-}
-
-impl IndentQueryPredicates {
- fn are_satisfied(
- &self,
- match_: &QueryMatch,
- text: RopeSlice,
- new_line_byte_pos: Option<u32>,
- ) -> bool {
- for (capture, not_expected_kind) in self.not_kind_eq.iter() {
- let node = match_.nodes_for_capture(*capture).next();
- if node.is_some_and(|n| n.kind() == not_expected_kind.as_ref()) {
- return false;
- }
- }
-
- if let Some((capture1, capture2, negated)) = self.same_line {
- let n1 = match_.nodes_for_capture(capture1).next();
- let n2 = match_.nodes_for_capture(capture2).next();
- let satisfied = n1.zip(n2).is_some_and(|(n1, n2)| {
- let n1_line = get_node_start_line(text, n1, new_line_byte_pos);
- let n2_line = get_node_start_line(text, n2, new_line_byte_pos);
- let same_line = n1_line == n2_line;
- same_line != negated
- });
-
- if !satisfied {
- return false;
- }
+/// Computes for node and all ancestors whether they are the first node on their line.
+/// The first entry in the return value represents the root node, the last one the node itself
+fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> {
+ let mut first_in_line = Vec::new();
+ loop {
+ if let Some(prev) = node.prev_sibling() {
+ // If we insert a new line, the first node at/after the cursor is considered to be the first in its line
+ let first = prev.end_position().row != node.start_position().row
+ || new_line_byte_pos.map_or(false, |byte_pos| {
+ node.start_byte() >= byte_pos && prev.start_byte() < byte_pos
+ });
+ first_in_line.push(Some(first));
+ } else {
+ // Nodes that have no previous siblings are first in their line if and only if their parent is
+ // (which we don't know yet)
+ first_in_line.push(None);
}
-
- if let Some((capture, negated)) = self.one_line {
- let node = match_.nodes_for_capture(capture).next();
- let satisfied = node.is_some_and(|node| {
- let start_line = get_node_start_line(text, node, new_line_byte_pos);
- let end_line = get_node_end_line(text, node, new_line_byte_pos);
- let one_line = end_line == start_line;
- one_line != negated
- });
-
- if !satisfied {
- return false;
- }
+ if let Some(parent) = node.parent() {
+ node = parent;
+ } else {
+ break;
}
-
- true
}
-}
-
-#[derive(Debug)]
-pub struct IndentQuery {
- query: Query,
- properties: HashMap<Pattern, IndentScope>,
- predicates: HashMap<Pattern, IndentQueryPredicates>,
- indent_capture: Option<Capture>,
- indent_always_capture: Option<Capture>,
- outdent_capture: Option<Capture>,
- outdent_always_capture: Option<Capture>,
- align_capture: Option<Capture>,
- anchor_capture: Option<Capture>,
- extend_capture: Option<Capture>,
- extend_prevent_once_capture: Option<Capture>,
-}
-
-impl IndentQuery {
- pub fn new(grammar: Grammar, source: &str) -> Result<Self, tree_sitter::query::ParseError> {
- let mut properties = HashMap::new();
- let mut predicates: HashMap<Pattern, IndentQueryPredicates> = HashMap::new();
- let query = Query::new(grammar, source, |pattern, predicate| match predicate {
- UserPredicate::SetProperty { key: "scope", val } => {
- let scope = match val {
- Some("all") => IndentScope::All,
- Some("tail") => IndentScope::Tail,
- Some(other) => {
- return Err(format!("unknown scope (#set! scope \"{other}\")").into())
- }
- None => return Err("missing scope value (#set! scope ...)".into()),
- };
-
- properties.insert(pattern, scope);
-
- Ok(())
- }
- UserPredicate::Other(predicate) => {
- let name = predicate.name();
- match name {
- "not-kind-eq?" => {
- predicate.check_arg_count(2)?;
- let capture = predicate.capture_arg(0)?;
- let not_expected_kind = predicate.str_arg(1)?;
-
- predicates
- .entry(pattern)
- .or_default()
- .not_kind_eq
- .push((capture, not_expected_kind.into()));
- Ok(())
- }
- "same-line?" | "not-same-line?" => {
- predicate.check_arg_count(2)?;
- let capture1 = predicate.capture_arg(0)?;
- let capture2 = predicate.capture_arg(1)?;
- let negated = name == "not-same-line?";
-
- predicates.entry(pattern).or_default().same_line =
- Some((capture1, capture2, negated));
- Ok(())
- }
- "one-line?" | "not-one-line?" => {
- predicate.check_arg_count(1)?;
- let capture = predicate.capture_arg(0)?;
- let negated = name == "not-one-line?";
- predicates.entry(pattern).or_default().one_line = Some((capture, negated));
- Ok(())
- }
- _ => Err(InvalidPredicateError::unknown(UserPredicate::Other(
- predicate,
- ))),
- }
- }
- _ => Err(InvalidPredicateError::unknown(predicate)),
- })?;
-
- Ok(Self {
- properties,
- predicates,
- indent_capture: query.get_capture("indent"),
- indent_always_capture: query.get_capture("indent.always"),
- outdent_capture: query.get_capture("outdent"),
- outdent_always_capture: query.get_capture("outdent.always"),
- align_capture: query.get_capture("align"),
- anchor_capture: query.get_capture("anchor"),
- extend_capture: query.get_capture("extend"),
- extend_prevent_once_capture: query.get_capture("extend.prevent-once"),
- query,
- })
+ let mut result = Vec::with_capacity(first_in_line.len());
+ let mut parent_is_first = true; // The root node is by definition the first node in its line
+ for first in first_in_line.into_iter().rev() {
+ if let Some(first) = first {
+ result.push(first);
+ parent_is_first = first;
+ } else {
+ result.push(parent_is_first);
+ }
}
+ result
}
/// The total indent for some line of code.
/// This is usually constructed in one of 2 ways:
/// - Successively add indent captures to get the (added) indent from a single line
/// - Successively add the indent results for each line
-/// The string that this indentation defines starts with the string contained in the align field (unless it is None), followed by:
+/// The string that this indentation defines starts with the string contained in the align field (unless it is None), followed by:
/// - max(0, indent - outdent) tabs, if tabs are used for indentation
/// - max(0, indent - outdent)*indent_width spaces, if spaces are used for indentation
#[derive(Default, Debug, PartialEq, Eq, Clone)]
@@ -575,7 +473,7 @@ enum IndentCaptureType<'a> {
Align(RopeSlice<'a>),
}
-impl IndentCaptureType<'_> {
+impl<'a> IndentCaptureType<'a> {
fn default_scope(&self) -> IndentScope {
match self {
IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail,
@@ -612,114 +510,193 @@ struct IndentQueryResult<'a> {
extend_captures: HashMap<usize, Vec<ExtendCapture>>,
}
-fn get_node_start_line(text: RopeSlice, node: &Node, new_line_byte_pos: Option<u32>) -> usize {
- let mut node_line = text.byte_to_line(node.start_byte() as usize);
+fn get_node_start_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
+ let mut node_line = node.start_position().row;
// Adjust for the new line that will be inserted
- if new_line_byte_pos.is_some_and(|pos| node.start_byte() >= pos) {
+ if new_line_byte_pos.map_or(false, |pos| node.start_byte() >= pos) {
node_line += 1;
}
node_line
}
-fn get_node_end_line(text: RopeSlice, node: &Node, new_line_byte_pos: Option<u32>) -> usize {
- let mut node_line = text.byte_to_line(node.end_byte() as usize);
+fn get_node_end_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
+ let mut node_line = node.end_position().row;
// Adjust for the new line that will be inserted (with a strict inequality since end_byte is exclusive)
- if new_line_byte_pos.is_some_and(|pos| node.end_byte() > pos) {
+ if new_line_byte_pos.map_or(false, |pos| node.end_byte() > pos) {
node_line += 1;
}
node_line
}
fn query_indents<'a>(
- query: &IndentQuery,
+ query: &Query,
syntax: &Syntax,
+ cursor: &mut QueryCursor,
text: RopeSlice<'a>,
- range: std::ops::Range<u32>,
- new_line_byte_pos: Option<u32>,
+ range: std::ops::Range<usize>,
+ new_line_byte_pos: Option<usize>,
) -> IndentQueryResult<'a> {
let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
-
- let mut cursor = InactiveQueryCursor::new(range, TREE_SITTER_MATCH_LIMIT).execute_query(
- &query.query,
- &syntax.tree().root_node(),
- RopeInput::new(text),
- );
+ cursor.set_byte_range(range);
// Iterate over all captures from the query
- while let Some(m) = cursor.next_match() {
+ for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
// Skip matches where not all custom predicates are fulfilled
- if query
- .predicates
- .get(&m.pattern())
- .is_some_and(|preds| !preds.are_satisfied(&m, text, new_line_byte_pos))
- {
+ if !query.general_predicates(m.pattern_index).iter().all(|pred| {
+ match pred.operator.as_ref() {
+ "not-kind-eq?" => match (pred.args.get(0), pred.args.get(1)) {
+ (
+ Some(QueryPredicateArg::Capture(capture_idx)),
+ Some(QueryPredicateArg::String(kind)),
+ ) => {
+ let node = m.nodes_for_capture_index(*capture_idx).next();
+ match node {
+ Some(node) => node.kind()!=kind.as_ref(),
+ _ => true,
+ }
+ }
+ _ => {
+ panic!("Invalid indent query: Arguments to \"not-kind-eq?\" must be a capture and a string");
+ }
+ },
+ "same-line?" | "not-same-line?" => {
+ match (pred.args.get(0), pred.args.get(1)) {
+ (
+ Some(QueryPredicateArg::Capture(capt1)),
+ Some(QueryPredicateArg::Capture(capt2))
+ ) => {
+ let n1 = m.nodes_for_capture_index(*capt1).next();
+ let n2 = m.nodes_for_capture_index(*capt2).next();
+ match (n1, n2) {
+ (Some(n1), Some(n2)) => {
+ let n1_line = get_node_start_line(n1, new_line_byte_pos);
+ let n2_line = get_node_start_line(n2, new_line_byte_pos);
+ let same_line = n1_line == n2_line;
+ same_line==(pred.operator.as_ref()=="same-line?")
+ }
+ _ => true,
+ }
+ }
+ _ => {
+ panic!("Invalid indent query: Arguments to \"{}\" must be 2 captures", pred.operator);
+ }
+ }
+ }
+ "one-line?" | "not-one-line?" => match pred.args.get(0) {
+ Some(QueryPredicateArg::Capture(capture_idx)) => {
+ let node = m.nodes_for_capture_index(*capture_idx).next();
+
+ match node {
+ Some(node) => {
+ let (start_line, end_line) = (get_node_start_line(node,new_line_byte_pos), get_node_end_line(node, new_line_byte_pos));
+ let one_line = end_line == start_line;
+ one_line != (pred.operator.as_ref() == "not-one-line?")
+ },
+ _ => true,
+ }
+ }
+ _ => {
+ panic!("Invalid indent query: Arguments to \"not-kind-eq?\" must be a capture and a string");
+ }
+ },
+ _ => {
+ panic!(
+ "Invalid indent query: Unknown predicate (\"{}\")",
+ pred.operator
+ );
+ }
+ }
+ }) {
continue;
}
// A list of pairs (node_id, indent_capture) that are added by this match.
// They cannot be added to indent_captures immediately since they may depend on other captures (such as an @anchor).
let mut added_indent_captures: Vec<(usize, IndentCapture)> = Vec::new();
// The row/column position of the optional anchor in this query
- let mut anchor: Option<&Node> = None;
- for matched_node in m.matched_nodes() {
- let node_id = matched_node.node.id();
- let capture = Some(matched_node.capture);
- let capture_type = if capture == query.indent_capture {
- IndentCaptureType::Indent
- } else if capture == query.indent_always_capture {
- IndentCaptureType::IndentAlways
- } else if capture == query.outdent_capture {
- IndentCaptureType::Outdent
- } else if capture == query.outdent_always_capture {
- IndentCaptureType::OutdentAlways
- } else if capture == query.align_capture {
- IndentCaptureType::Align(RopeSlice::from(""))
- } else if capture == query.anchor_capture {
- if anchor.is_some() {
- log::error!("Invalid indent query: Encountered more than one @anchor in the same match.")
- } else {
- anchor = Some(&matched_node.node);
+ let mut anchor: Option<tree_sitter::Node> = None;
+ for capture in m.captures {
+ let capture_name = query.capture_names()[capture.index as usize].as_str();
+ let capture_type = match capture_name {
+ "indent" => IndentCaptureType::Indent,
+ "indent.always" => IndentCaptureType::IndentAlways,
+ "outdent" => IndentCaptureType::Outdent,
+ "outdent.always" => IndentCaptureType::OutdentAlways,
+ // The alignment will be updated to the correct value at the end, when the anchor is known.
+ "align" => IndentCaptureType::Align(RopeSlice::from("")),
+ "anchor" => {
+ if anchor.is_some() {
+ log::error!("Invalid indent query: Encountered more than one @anchor in the same match.")
+ } else {
+ anchor = Some(capture.node);
+ }
+ continue;
+ }
+ "extend" => {
+ extend_captures
+ .entry(capture.node.id())
+ .or_insert_with(|| Vec::with_capacity(1))
+ .push(ExtendCapture::Extend);
+ continue;
+ }
+ "extend.prevent-once" => {
+ extend_captures
+ .entry(capture.node.id())
+ .or_insert_with(|| Vec::with_capacity(1))
+ .push(ExtendCapture::PreventOnce);
+ continue;
+ }
+ _ => {
+ // Ignore any unknown captures (these may be needed for predicates such as #match?)
+ continue;
}
- continue;
- } else if capture == query.extend_capture {
- extend_captures
- .entry(node_id)
- .or_insert_with(|| Vec::with_capacity(1))
- .push(ExtendCapture::Extend);
- continue;
- } else if capture == query.extend_prevent_once_capture {
- extend_captures
- .entry(node_id)
- .or_insert_with(|| Vec::with_capacity(1))
- .push(ExtendCapture::PreventOnce);
- continue;
- } else {
- // Ignore any unknown captures (these may be needed for predicates such as #match?)
- continue;
};
-
- // Apply additional settings for this capture
- let scope = query
- .properties
- .get(&m.pattern())
- .copied()
- .unwrap_or_else(|| capture_type.default_scope());
- let indent_capture = IndentCapture {
+ let scope = capture_type.default_scope();
+ let mut indent_capture = IndentCapture {
capture_type,
scope,
};
- added_indent_captures.push((node_id, indent_capture))
+ // Apply additional settings for this capture
+ for property in query.property_settings(m.pattern_index) {
+ match property.key.as_ref() {
+ "scope" => {
+ indent_capture.scope = match property.value.as_deref() {
+ Some("all") => IndentScope::All,
+ Some("tail") => IndentScope::Tail,
+ Some(s) => {
+ panic!("Invalid indent query: Unknown value for \"scope\" property (\"{}\")", s);
+ }
+ None => {
+ panic!(
+ "Invalid indent query: Missing value for \"scope\" property"
+ );
+ }
+ }
+ }
+ _ => {
+ panic!(
+ "Invalid indent query: Unknown property \"{}\"",
+ property.key
+ );
+ }
+ }
+ }
+ added_indent_captures.push((capture.node.id(), indent_capture))
}
for (node_id, mut capture) in added_indent_captures {
// Set the anchor for all align queries.
if let IndentCaptureType::Align(_) = capture.capture_type {
- let Some(anchor) = anchor else {
- log::error!("Invalid indent query: @align requires an accompanying @anchor.");
- continue;
+ let anchor = match anchor {
+ None => {
+ log::error!(
+ "Invalid indent query: @align requires an accompanying @anchor."
+ );
+ continue;
+ }
+ Some(anchor) => anchor,
};
- let line = text.byte_to_line(anchor.start_byte() as usize);
- let line_start = text.line_to_byte(line);
capture.capture_type = IndentCaptureType::Align(
- text.byte_slice(line_start..anchor.start_byte() as usize),
+ text.line(anchor.start_position().row)
+ .byte_slice(0..anchor.start_position().column),
);
}
indent_captures
@@ -771,15 +748,13 @@ fn extend_nodes<'a>(
// - the cursor is on the same line as the end of the node OR
// - the line that the cursor is on is more indented than the
// first line of the node
- if text.byte_to_line(deepest_preceding.end_byte() as usize) == line {
+ if deepest_preceding.end_position().row == line {
extend_node = true;
} else {
let cursor_indent =
indent_level_for_line(text.line(line), tab_width, indent_width);
let node_indent = indent_level_for_line(
- text.line(
- text.byte_to_line(deepest_preceding.start_byte() as usize),
- ),
+ text.line(deepest_preceding.start_position().row),
tab_width,
indent_width,
);
@@ -796,7 +771,7 @@ fn extend_nodes<'a>(
if node_captured && stop_extend {
stop_extend = false;
} else if extend_node && !stop_extend {
- *node = deepest_preceding.clone();
+ *node = deepest_preceding;
break;
}
// If the tree contains a syntax error, `deepest_preceding` may not
@@ -813,17 +788,17 @@ fn extend_nodes<'a>(
/// - The indent captures for all relevant nodes.
#[allow(clippy::too_many_arguments)]
fn init_indent_query<'a, 'b>(
- query: &IndentQuery,
+ query: &Query,
syntax: &'a Syntax,
text: RopeSlice<'b>,
tab_width: usize,
indent_width: usize,
line: usize,
- byte_pos: u32,
- new_line_byte_pos: Option<u32>,
+ byte_pos: usize,
+ new_line_byte_pos: Option<usize>,
) -> Option<(Node<'a>, HashMap<usize, Vec<IndentCapture<'b>>>)> {
// The innermost tree-sitter node which is considered for the indent
- // computation. It may change if some preceding node is extended
+ // computation. It may change if some predeceding node is extended
let mut node = syntax
.tree()
.root_node()
@@ -833,25 +808,37 @@ fn init_indent_query<'a, 'b>(
// The query range should intersect with all nodes directly preceding
// the position of the indent query in case one of them is extended.
let mut deepest_preceding = None; // The deepest node preceding the indent query position
- for child in node.children() {
+ let mut tree_cursor = node.walk();
+ for child in node.children(&mut tree_cursor) {
if child.byte_range().end <= byte_pos {
- deepest_preceding = Some(child.clone());
+ deepest_preceding = Some(child);
}
}
deepest_preceding = deepest_preceding.map(|mut prec| {
// Get the deepest directly preceding node
while prec.child_count() > 0 {
- prec = prec.child(prec.child_count() - 1).unwrap().clone();
+ prec = prec.child(prec.child_count() - 1).unwrap();
}
prec
});
let query_range = deepest_preceding
- .as_ref()
.map(|prec| prec.byte_range().end - 1..byte_pos + 1)
.unwrap_or(byte_pos..byte_pos + 1);
- let query_result = query_indents(query, syntax, text, query_range, new_line_byte_pos);
- (query_result, deepest_preceding)
+ crate::syntax::PARSER.with(|ts_parser| {
+ let mut ts_parser = ts_parser.borrow_mut();
+ let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
+ let query_result = query_indents(
+ query,
+ syntax,
+ &mut cursor,
+ text,
+ query_range,
+ new_line_byte_pos,
+ );
+ ts_parser.cursors.push(cursor);
+ (query_result, deepest_preceding)
+ })
};
let extend_captures = query_result.extend_captures;
@@ -886,7 +873,6 @@ fn init_indent_query<'a, 'b>(
/// - The line after the node. This is defined by:
/// - The scope `tail`.
/// - The scope `all` if this node is not the first node on its line.
-///
/// Intuitively, `all` applies to everything contained in this node while `tail` applies to everything except for the first line of the node.
/// The indents from different nodes for the same line are then combined.
/// The result [Indentation] is simply the sum of the [Indentation] for all lines.
@@ -909,7 +895,7 @@ fn init_indent_query<'a, 'b>(
/// ```
#[allow(clippy::too_many_arguments)]
pub fn treesitter_indent_for_pos<'a>(
- query: &IndentQuery,
+ query: &Query,
syntax: &Syntax,
tab_width: usize,
indent_width: usize,
@@ -918,7 +904,7 @@ pub fn treesitter_indent_for_pos<'a>(
pos: usize,
new_line: bool,
) -> Option<Indentation<'a>> {
- let byte_pos = text.char_to_byte(pos) as u32;
+ let byte_pos = text.char_to_byte(pos);
let new_line_byte_pos = new_line.then_some(byte_pos);
let (mut node, mut indent_captures) = init_indent_query(
query,
@@ -930,6 +916,7 @@ pub fn treesitter_indent_for_pos<'a>(
byte_pos,
new_line_byte_pos,
)?;
+ let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
let mut result = Indentation::default();
// We always keep track of all the indent changes on one line, in order to only indent once
@@ -938,7 +925,9 @@ pub fn treesitter_indent_for_pos<'a>(
let mut indent_for_line_below = Indentation::default();
loop {
- let is_first = is_first_in_line(&node, text, new_line_byte_pos);
+ // This can safely be unwrapped because `first_in_line` contains
+ // one entry for each ancestor of the node (which is what we iterate over)
+ let is_first = *first_in_line.last().unwrap();
// Apply all indent definitions for this node.
// Since we only iterate over each node once, we can remove the
@@ -961,8 +950,8 @@ pub fn treesitter_indent_for_pos<'a>(
}
if let Some(parent) = node.parent() {
- let node_line = get_node_start_line(text, &node, new_line_byte_pos);
- let parent_line = get_node_start_line(text, &parent, new_line_byte_pos);
+ let node_line = get_node_start_line(node, new_line_byte_pos);
+ let parent_line = get_node_start_line(parent, new_line_byte_pos);
if node_line != parent_line {
// Don't add indent for the line below the line of the query
@@ -981,12 +970,12 @@ pub fn treesitter_indent_for_pos<'a>(
}
node = parent;
+ first_in_line.pop();
} else {
// Only add the indentation for the line below if that line
// is not after the line that the indentation is calculated for.
- let node_start_line = text.byte_to_line(node.start_byte() as usize);
- if node_start_line < line
- || (new_line && node_start_line == line && node.start_byte() < byte_pos)
+ if (node.start_position().row < line)
+ || (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
{
result.add_line(indent_for_line_below);
}
@@ -1001,7 +990,7 @@ pub fn treesitter_indent_for_pos<'a>(
/// This is done either using treesitter, or if that's not available by copying the indentation from the current line
#[allow(clippy::too_many_arguments)]
pub fn indent_for_newline(
- loader: &syntax::Loader,
+ language_config: Option<&LanguageConfiguration>,
syntax: Option<&Syntax>,
indent_heuristic: &IndentationHeuristic,
indent_style: &IndentStyle,
@@ -1018,7 +1007,7 @@ pub fn indent_for_newline(
Some(syntax),
) = (
indent_heuristic,
- syntax.and_then(|syntax| loader.indent_query(syntax.root_language())),
+ language_config.and_then(|config| config.indent_query()),
syntax,
) {
if let Some(indent) = treesitter_indent_for_pos(
@@ -1045,7 +1034,7 @@ pub fn indent_for_newline(
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 line.first_non_whitespace_char() {
+ let first_non_whitespace_char = match find_first_non_whitespace_char(line) {
Some(i) => i,
None => {
continue;
@@ -1086,10 +1075,10 @@ pub fn indent_for_newline(
indent_style.as_str().repeat(indent_level)
}
-pub fn get_scopes<'a>(syntax: Option<&'a Syntax>, text: RopeSlice, pos: usize) -> Vec<&'a str> {
+pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> {
let mut scopes = Vec::new();
if let Some(syntax) = syntax {
- let pos = text.char_to_byte(pos) as u32;
+ let pos = text.char_to_byte(pos);
let mut node = match syntax
.tree()
.root_node()