From 76378a1bfa7c38526e17537437d0d4d4088b88e6 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Thu, 20 Jan 2022 18:22:31 +0100 Subject: [PATCH 01/19] WIP: Rework indentation system --- helix-core/src/indent.rs | 337 ++++++++++++++++++++++-------- helix-core/src/syntax.rs | 79 ++++++- helix-term/src/commands.rs | 28 ++- runtime/queries/go/indents.toml | 4 +- runtime/queries/rust/indents.toml | 4 +- 5 files changed, 345 insertions(+), 107 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index a8ea30124e3a..433efc2cae94 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,6 +1,6 @@ use crate::{ chars::{char_is_line_ending, char_is_whitespace}, - syntax::{IndentQuery, LanguageConfiguration, Syntax}, + syntax::{IndentQuery, IndentQueryNode, IndentQueryScopes, LanguageConfiguration, Syntax}, tree_sitter::Node, Rope, RopeSlice, }; @@ -186,108 +186,262 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { len / tab_width } -/// Find the highest syntax node at position. -/// This is to identify the column where this node (e.g., an HTML closing tag) ends. -fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option { - let tree = syntax.tree(); +struct AddedIndent { + indent: bool, + outdent: bool, +} +impl AddedIndent { + fn new() -> Self { + AddedIndent { + indent: false, + outdent: false, + } + } + fn combine_with(&mut self, other: &AddedIndent) { + self.indent |= other.indent; + self.outdent |= other.outdent; + } +} - // named_descendant - let mut node = match tree.root_node().descendant_for_byte_range(pos, pos) { - Some(node) => node, - None => return None, - }; +struct IndentResult { + indent: i32, +} +impl IndentResult { + fn new() -> Self { + IndentResult { indent: 0 } + } + fn add(&mut self, added: &AddedIndent) { + if added.indent && !added.outdent { + eprintln!("Indent result"); + self.indent += 1; + } else if added.outdent && !added.indent { + eprintln!("Outdent result"); + self.indent = self.indent.saturating_sub(1); + } + } + fn as_string(&self, indent_style: &IndentStyle) -> String { + indent_style + .as_str() + .repeat(std::cmp::max(self.indent, 0) as usize) + } +} + +// Get the node where to start the indent query (this is usually just the lowest node containing byte_pos) +fn get_lowest_node<'a>(root: Node<'a>, query: &IndentQuery, byte_pos: usize) -> Option> { + root.descendant_for_byte_range(byte_pos, byte_pos) + // TODO Special handling for languages like python +} - while let Some(parent) = node.parent() { - if parent.start_byte() == node.start_byte() { - node = parent +// 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, byte_pos: usize, new_line: bool) -> Vec { + 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 && 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 iff their parent is + // (which we don't know yet) + first_in_line.push(None); + } + if let Some(parent) = node.parent() { + node = parent; } else { break; } } - Some(node) + 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 } -/// 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, - line: usize, - newline: bool, -) -> usize { - let mut increment: isize = 0; +// This assumes that the name matches and checks for all the other conditions +fn matches(query_node: &IndentQueryNode, node: Node) -> bool { + match query_node { + IndentQueryNode::SimpleNode(_) => true, + } +} - let mut node = match node { - Some(node) => node, - None => return 0, - }; +fn contains_match(scope: &[IndentQueryNode], node: Node) -> bool { + for unnamed_node in scope.iter().take_while(|n| n.name().is_none()) { + if matches(unnamed_node, node) { + return true; + } + } + let current_kind = node.kind(); + let first = scope.partition_point(|n| n.name() < Some(current_kind)); + for named_node in scope[first..] + .iter() + .take_while(|n| n.name() == Some(current_kind)) + { + if matches(named_node, node) { + return true; + } + } + false +} - let mut current_line = line; - let mut consider_indent = newline; - let mut increment_from_line: isize = 0; +fn scopes_contain_match(scopes: &IndentQueryScopes, node: Node) -> (bool, bool) { + let match_for_line = contains_match(&scopes.all, node); + let match_for_next = contains_match(&scopes.tail, node); + (match_for_line, match_for_next) +} - 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; - } +// The added indent for the line of the node and the next line +fn added_indent(query: &IndentQuery, node: Node) -> (AddedIndent, AddedIndent) { + let (indent, next_indent) = scopes_contain_match(&query.indent, node); + let (outdent, next_outdent) = scopes_contain_match(&query.outdent, node); + let line = AddedIndent { indent, outdent }; + let next = AddedIndent { + indent: next_indent, + outdent: next_outdent, + }; + (line, next) +} - if query.outdent.contains(node_kind) { - increment_from_line -= 1; +fn treesitter_indent_for_pos( + query: &IndentQuery, + syntax: &Syntax, + indent_style: &IndentStyle, + text: RopeSlice, + line: usize, + pos: usize, + new_line: bool, +) -> Option { + let byte_pos = text.char_to_byte(pos); + let mut node = match get_lowest_node(syntax.tree().root_node(), query, byte_pos) { + Some(n) => n, + None => { + return None; } - if query.indent.contains(node_kind) { - increment_from_line += 1; + }; + let mut first_in_line = get_first_in_line(node, byte_pos, new_line); + + let mut result = IndentResult::new(); + // We always keep track of all the indent changes on one line, in order to only indent once + // even if there are multiple "indent" nodes on the same line + let mut indent_for_line = AddedIndent::new(); + let mut indent_for_line_below = AddedIndent::new(); + eprintln!("START INDENT"); + loop { + eprintln!("Node {}", node.kind()); + let node_indents = added_indent(query, node); + if *first_in_line.last().unwrap() { + indent_for_line.combine_with(&node_indents.0); + } else { + indent_for_line_below.combine_with(&node_indents.0); } + indent_for_line_below.combine_with(&node_indents.1); if let Some(parent) = node.parent() { + let mut node_line = node.start_position().row; + let mut parent_line = parent.start_position().row; + if node.start_position().row == line && new_line { + // Also consider the line that will be inserted + if node.start_byte() >= byte_pos { + node_line += 1; + } + if parent.start_byte() >= byte_pos { + parent_line += 1; + } + }; + if node_line != parent_line { + if node_line < line + (new_line as usize) { + // Don't add indent for the line below the line of the query + eprintln!("Add line below"); + result.add(&indent_for_line_below); + } + if node_line == parent_line + 1 { + indent_for_line_below = indent_for_line; + indent_for_line = AddedIndent::new(); + } else { + eprintln!("Add line"); + result.add(&indent_for_line); + indent_for_line_below = AddedIndent::new(); + indent_for_line = AddedIndent::new(); + } + } + node = parent; + first_in_line.pop(); } else { + result.add(&indent_for_line_below); + result.add(&indent_for_line); break; } } - if consider_indent || increment_from_line < 0 { - increment += increment_from_line.signum(); - } - increment.max(0) as usize + Some(result.as_string(indent_style)) } -// 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 -pub fn suggested_indent_for_pos( +// Returns the indentation for a new line. +// This is done either using treesitter, or if that's not available by copying the indentation from the current line +pub fn indent_for_newline( language_config: Option<&LanguageConfiguration>, syntax: Option<&Syntax>, + indent_style: &IndentStyle, + tab_width: usize, text: RopeSlice, - pos: usize, - line: usize, - new_line: bool, -) -> Option { + line_before: usize, + line_before_end_pos: usize, + current_line: usize, +) -> String { 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); - // TODO: special case for comments - // TODO: if preserve_leading_whitespace - Some(calculate_indentation(query, node, line, new_line)) - } else { - None + if let Some(indent) = treesitter_indent_for_pos( + query, + syntax, + indent_style, + text, + line_before, + line_before_end_pos, + true, + ) { + return indent; + }; } + + let indent_level = indent_level_for_line(text.line(current_line), tab_width); + indent_style.as_str().repeat(indent_level) } +// 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 +//pub fn suggested_indent_for_pos( +// language_config: Option<&LanguageConfiguration>, +// syntax: Option<&Syntax>, +// text: RopeSlice, +// pos: usize, +// line: usize, +// new_line: bool, +//) -> Option { +// if let (Some(query), Some(syntax)) = ( +// language_config.and_then(|config| config.indent_query()), +// syntax, +// ) { +// let byte_start = text.char_to_byte(pos); +// // TODO: special case for comments +// // TODO: if preserve_leading_whitespace +// treesitter_indent_for_pos(query, syntax, text, byte_start, line, new_line) +// } else { +// None +// } +//} + pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> { let mut scopes = Vec::new(); if let Some(syntax) = syntax { @@ -456,26 +610,43 @@ where let highlight_config = language_config.highlight_config(&[]).unwrap(); let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)); let text = doc.slice(..); - let tab_width = 4; for i in 0..doc.len_lines() { let line = text.line(i); 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 {}: \"{}\"", + let suggested_indent = treesitter_indent_for_pos( + &language_config.indent_query().unwrap(), + &syntax, + &IndentStyle::Spaces(4), + text, + i, + text.line_to_char(i) + pos, + false, + ) + .unwrap(); + assert!( + line.get_slice(..suggested_indent.chars().count()) + .map_or(false, |s| s == suggested_indent), + "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n", i, - line + line.slice(..line.len_chars()-1), + suggested_indent, ); + //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 + //); } } } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index d6ec761003c1..07018b10ffe6 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -106,15 +106,86 @@ pub struct IndentationConfiguration { pub unit: String, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum IndentQueryNode { + // A node given just by its name + SimpleNode(String), +} +impl IndentQueryNode { + pub fn name<'a>(&'a self) -> Option<&'a str> { + match self { + IndentQueryNode::SimpleNode(n) => Some(&n), + } + } +} +impl PartialEq for IndentQueryNode { + fn eq(&self, other: &Self) -> bool { + self.name().eq(&other.name()) + } +} +impl Eq for IndentQueryNode {} +impl PartialOrd for IndentQueryNode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for IndentQueryNode { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.name().cmp(&other.name()) + } +} + +#[derive(Default, Debug, Serialize)] +pub struct IndentQueryScopes { + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub all: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub tail: Vec, +} + +impl IndentQueryScopes { + pub fn sort_nodes(&mut self) {} +} + +impl<'de> Deserialize<'de> for IndentQueryScopes { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Derived { + #[serde(default)] + pub all: Vec, + #[serde(default)] + pub tail: Vec, + } + let derived = Derived::deserialize(deserializer)?; + let mut result = IndentQueryScopes { + all: derived.all, + tail: derived.tail, + }; + result.all.sort_unstable(); + result.tail.sort_unstable(); + Ok(result) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct IndentQuery { + // #[serde(default)] + // #[serde(skip_serializing_if = "HashSet::is_empty")] + // pub indent: HashSet, + // #[serde(default)] + // #[serde(skip_serializing_if = "HashSet::is_empty")] + // pub outdent: HashSet, #[serde(default)] - #[serde(skip_serializing_if = "HashSet::is_empty")] - pub indent: HashSet, + pub indent: IndentQueryScopes, #[serde(default)] - #[serde(skip_serializing_if = "HashSet::is_empty")] - pub outdent: HashSet, + pub outdent: IndentQueryScopes, } #[derive(Debug)] diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fc0db6ed0c64..bd9cdaa5c13b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3805,17 +3805,16 @@ fn open(cx: &mut Context, open: Open) { ) }; - // TODO: share logic with insert_newline for indentation - let indent_level = indent::suggested_indent_for_pos( + let indent = indent::indent_for_newline( doc.language_config(), doc.syntax(), + &doc.indent_style, + doc.tab_width(), 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); + line_end_index, + cursor_line, + ); let indent_len = indent.len(); let mut text = String::with_capacity(1 + indent_len); text.push_str(doc.line_ending.as_str()); @@ -4550,23 +4549,20 @@ pub mod insert { let curr = contents.get_char(pos).unwrap_or(' '); let current_line = text.char_to_line(pos); - let indent_level = indent::suggested_indent_for_pos( + let indent = indent::indent_for_newline( doc.language_config(), doc.syntax(), + &doc.indent_style, + doc.tab_width(), text, + current_line, pos, current_line, - true, - ) - .unwrap_or_else(|| { - indent::indent_level_for_line(text.line(current_line), doc.tab_width()) - }); - - 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); + let inner_indent = indent.clone() + doc.indent_style.as_str(); text.reserve_exact(2 + indent.len() + inner_indent.len()); text.push_str(doc.line_ending.as_str()); text.push_str(&inner_indent); diff --git a/runtime/queries/go/indents.toml b/runtime/queries/go/indents.toml index 7929ff50d37b..3a7a5dcd6607 100644 --- a/runtime/queries/go/indents.toml +++ b/runtime/queries/go/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "import_declaration", "const_declaration", #"var_declaration", @@ -22,7 +22,7 @@ indent = [ "block", ] -outdent = [ +outdent.all = [ "case", "}", "]", diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml index 51a0ceeaf014..c1b7461359be 100644 --- a/runtime/queries/rust/indents.toml +++ b/runtime/queries/rust/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "use_list", "block", "match_block", @@ -20,7 +20,7 @@ indent = [ "macro_invocation" ] -outdent = [ +outdent.all = [ "where", "}", "]", From 3e39f7a957847d81bf1896670d673c2ba5424a02 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sat, 22 Jan 2022 18:04:35 +0100 Subject: [PATCH 02/19] Add ComplexNode for context-aware indentation (including a proof of concept for assignment statements in rust) --- helix-core/src/indent.rs | 101 ++++++++++++++++++++++-------- helix-core/src/syntax.rs | 17 +++-- runtime/queries/rust/indents.toml | 10 ++- 3 files changed, 95 insertions(+), 33 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 433efc2cae94..ae7aa46759bf 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,3 +1,5 @@ +use tree_sitter::TreeCursor; + use crate::{ chars::{char_is_line_ending, char_is_whitespace}, syntax::{IndentQuery, IndentQueryNode, IndentQueryScopes, LanguageConfiguration, Syntax}, @@ -212,10 +214,8 @@ impl IndentResult { } fn add(&mut self, added: &AddedIndent) { if added.indent && !added.outdent { - eprintln!("Indent result"); self.indent += 1; } else if added.outdent && !added.indent { - eprintln!("Outdent result"); self.indent = self.indent.saturating_sub(1); } } @@ -227,7 +227,7 @@ impl IndentResult { } // Get the node where to start the indent query (this is usually just the lowest node containing byte_pos) -fn get_lowest_node<'a>(root: Node<'a>, query: &IndentQuery, byte_pos: usize) -> Option> { +fn get_lowest_node<'a>(root: Node<'a>, _query: &IndentQuery, byte_pos: usize) -> Option> { root.descendant_for_byte_range(byte_pos, byte_pos) // TODO Special handling for languages like python } @@ -268,41 +268,86 @@ fn get_first_in_line(mut node: Node, byte_pos: usize, new_line: bool) -> Vec bool { +fn matches<'a>(query_node: &IndentQueryNode, node: Node<'a>, cursor: &mut TreeCursor<'a>) -> bool { match query_node { IndentQueryNode::SimpleNode(_) => true, + IndentQueryNode::ComplexNode { + kind: _, + field_name, + parent_kind_in, + } => { + if let Some(parent_kind_in) = parent_kind_in { + let parent_matches = node.parent().map_or(false, |p| { + parent_kind_in.iter().any(|kind| kind.as_str() == p.kind()) + }); + if !parent_matches { + return false; + } + } + if let Some(field_name) = field_name { + let parent = match node.parent() { + None => { + return false; + } + Some(p) => p, + }; + let mut found_child = false; + for child in parent.children_by_field_name(field_name, cursor) { + if child == node { + found_child = true; + break; + } + } + if !found_child { + return false; + } + } + true + } } } -fn contains_match(scope: &[IndentQueryNode], node: Node) -> bool { - for unnamed_node in scope.iter().take_while(|n| n.name().is_none()) { - if matches(unnamed_node, node) { - return true; - } - } +fn contains_match<'a>( + scope: &[IndentQueryNode], + node: Node<'a>, + cursor: &mut TreeCursor<'a>, +) -> bool { let current_kind = node.kind(); - let first = scope.partition_point(|n| n.name() < Some(current_kind)); + let first = scope.partition_point(|n| n.kind() < Some(current_kind)); for named_node in scope[first..] .iter() - .take_while(|n| n.name() == Some(current_kind)) + .take_while(|n| n.kind() == Some(current_kind)) { - if matches(named_node, node) { + if matches(named_node, node, cursor) { + return true; + } + } + for unnamed_node in scope.iter().take_while(|n| n.kind().is_none()) { + if matches(unnamed_node, node, cursor) { return true; } } false } -fn scopes_contain_match(scopes: &IndentQueryScopes, node: Node) -> (bool, bool) { - let match_for_line = contains_match(&scopes.all, node); - let match_for_next = contains_match(&scopes.tail, node); +fn scopes_contain_match<'a>( + scopes: &IndentQueryScopes, + node: Node<'a>, + cursor: &mut TreeCursor<'a>, +) -> (bool, bool) { + let match_for_line = contains_match(&scopes.all, node, cursor); + let match_for_next = contains_match(&scopes.tail, node, cursor); (match_for_line, match_for_next) } // The added indent for the line of the node and the next line -fn added_indent(query: &IndentQuery, node: Node) -> (AddedIndent, AddedIndent) { - let (indent, next_indent) = scopes_contain_match(&query.indent, node); - let (outdent, next_outdent) = scopes_contain_match(&query.outdent, node); +fn added_indent<'a>( + query: &IndentQuery, + node: Node<'a>, + cursor: &mut TreeCursor<'a>, +) -> (AddedIndent, AddedIndent) { + let (indent, next_indent) = scopes_contain_match(&query.indent, node, cursor); + let (outdent, next_outdent) = scopes_contain_match(&query.outdent, node, cursor); let line = AddedIndent { indent, outdent }; let next = AddedIndent { indent: next_indent, @@ -320,6 +365,7 @@ fn treesitter_indent_for_pos( pos: usize, new_line: bool, ) -> Option { + let mut cursor = syntax.tree().walk(); let byte_pos = text.char_to_byte(pos); let mut node = match get_lowest_node(syntax.tree().root_node(), query, byte_pos) { Some(n) => n, @@ -334,10 +380,8 @@ fn treesitter_indent_for_pos( // even if there are multiple "indent" nodes on the same line let mut indent_for_line = AddedIndent::new(); let mut indent_for_line_below = AddedIndent::new(); - eprintln!("START INDENT"); loop { - eprintln!("Node {}", node.kind()); - let node_indents = added_indent(query, node); + let node_indents = added_indent(query, node, &mut cursor); if *first_in_line.last().unwrap() { indent_for_line.combine_with(&node_indents.0); } else { @@ -360,18 +404,15 @@ fn treesitter_indent_for_pos( if node_line != parent_line { if node_line < line + (new_line as usize) { // Don't add indent for the line below the line of the query - eprintln!("Add line below"); result.add(&indent_for_line_below); } if node_line == parent_line + 1 { indent_for_line_below = indent_for_line; - indent_for_line = AddedIndent::new(); } else { - eprintln!("Add line"); result.add(&indent_for_line); indent_for_line_below = AddedIndent::new(); - indent_for_line = AddedIndent::new(); } + indent_for_line = AddedIndent::new(); } node = parent; @@ -387,6 +428,7 @@ fn treesitter_indent_for_pos( // Returns the indentation for a new line. // 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( language_config: Option<&LanguageConfiguration>, syntax: Option<&Syntax>, @@ -500,6 +542,13 @@ mod test { let does_indentation_work = 1; + let mut really_long_variable_name_using_up_the_line = + really_long_fn_that_should_definitely_go_on_the_next_line(); + really_long_variable_name_using_up_the_line = + really_long_fn_that_should_definitely_go_on_the_next_line(); + really_long_variable_name_using_up_the_line |= + really_long_fn_that_should_definitely_go_on_the_next_line(); + let test_function = function_with_param(this_param, that_param ); diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 07018b10ffe6..d6dda32a4390 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -109,19 +109,26 @@ pub struct IndentationConfiguration { #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum IndentQueryNode { - // A node given just by its name + // A node given just by its kind SimpleNode(String), + // A node given by a list of characteristics which must all be fulfilled in order to match + ComplexNode { + kind: Option, + field_name: Option, + parent_kind_in: Option>, + }, } impl IndentQueryNode { - pub fn name<'a>(&'a self) -> Option<&'a str> { + pub fn kind(&self) -> Option<&str> { match self { - IndentQueryNode::SimpleNode(n) => Some(&n), + IndentQueryNode::SimpleNode(n) => Some(n), + IndentQueryNode::ComplexNode { kind, .. } => kind.as_ref().map(|k| k.as_str()), } } } impl PartialEq for IndentQueryNode { fn eq(&self, other: &Self) -> bool { - self.name().eq(&other.name()) + self.kind().eq(&other.kind()) } } impl Eq for IndentQueryNode {} @@ -132,7 +139,7 @@ impl PartialOrd for IndentQueryNode { } impl Ord for IndentQueryNode { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.name().cmp(&other.name()) + self.kind().cmp(&other.kind()) } } diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml index c1b7461359be..bdf0e10a99dd 100644 --- a/runtime/queries/rust/indents.toml +++ b/runtime/queries/rust/indents.toml @@ -1,4 +1,9 @@ -indent.tail = [ +[indent] +all = [ + { parent_kind_in = ["assignment_expression", "compound_assignment_expr"], field_name = "right"}, + { parent_kind_in = ["let_declaration"], field_name = "value" } +] +tail = [ "use_list", "block", "match_block", @@ -20,7 +25,8 @@ indent.tail = [ "macro_invocation" ] -outdent.all = [ +[outdent] +all = [ "where", "}", "]", From c417b9e2485a0465da2cabf492f810a53cc1c6d1 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sat, 22 Jan 2022 18:12:20 +0100 Subject: [PATCH 03/19] Add switch statements to Go indents.toml (fixes the second half of issue #1523) Remove commented-out code --- helix-core/src/indent.rs | 39 --------------------------------- helix-core/src/syntax.rs | 6 ----- runtime/queries/go/indents.toml | 2 ++ 3 files changed, 2 insertions(+), 45 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index ae7aa46759bf..b8f0d0a84961 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -460,30 +460,6 @@ pub fn indent_for_newline( indent_style.as_str().repeat(indent_level) } -// 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 -//pub fn suggested_indent_for_pos( -// language_config: Option<&LanguageConfiguration>, -// syntax: Option<&Syntax>, -// text: RopeSlice, -// pos: usize, -// line: usize, -// new_line: bool, -//) -> Option { -// if let (Some(query), Some(syntax)) = ( -// language_config.and_then(|config| config.indent_query()), -// syntax, -// ) { -// let byte_start = text.char_to_byte(pos); -// // TODO: special case for comments -// // TODO: if preserve_leading_whitespace -// treesitter_indent_for_pos(query, syntax, text, byte_start, line, new_line) -// } else { -// None -// } -//} - pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> { let mut scopes = Vec::new(); if let Some(syntax) = syntax { @@ -681,21 +657,6 @@ where line.slice(..line.len_chars()-1), suggested_indent, ); - //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 - //); } } } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index d6dda32a4390..08ecbf7ccb77 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -183,12 +183,6 @@ impl<'de> Deserialize<'de> for IndentQueryScopes { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct IndentQuery { - // #[serde(default)] - // #[serde(skip_serializing_if = "HashSet::is_empty")] - // pub indent: HashSet, - // #[serde(default)] - // #[serde(skip_serializing_if = "HashSet::is_empty")] - // pub outdent: HashSet, #[serde(default)] pub indent: IndentQueryScopes, #[serde(default)] diff --git a/runtime/queries/go/indents.toml b/runtime/queries/go/indents.toml index 3a7a5dcd6607..cd0d5465c6ee 100644 --- a/runtime/queries/go/indents.toml +++ b/runtime/queries/go/indents.toml @@ -20,6 +20,8 @@ indent.tail = [ "argument_list", "field_declaration_list", "block", + "type_switch_statement", + "expression_switch_statement" ] outdent.all = [ From eac73ebe96c56cea971d6ec5e1a1e444b0ff76fe Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sun, 23 Jan 2022 15:49:22 +0100 Subject: [PATCH 04/19] Migrate all existing indentation queries. Add more options to ComplexNode and use them to improve C/C++ indentation. --- helix-core/src/indent.rs | 56 +++++++++++++++++----- helix-core/src/syntax.rs | 3 +- runtime/queries/c/indents.toml | 11 ++++- runtime/queries/cmake/indents.toml | 4 +- runtime/queries/cpp/indents.toml | 11 ++++- runtime/queries/dart/indents.toml | 4 +- runtime/queries/fish/indents.toml | 4 +- runtime/queries/glsl/indents.toml | 4 +- runtime/queries/javascript/indents.toml | 4 +- runtime/queries/json/indents.toml | 4 +- runtime/queries/llvm-mir-yaml/indents.toml | 2 +- runtime/queries/llvm-mir/indents.toml | 4 +- runtime/queries/llvm/indents.toml | 4 +- runtime/queries/lua/indents.toml | 2 +- runtime/queries/nix/indents.toml | 4 +- runtime/queries/ocaml/indents.toml | 4 +- runtime/queries/perl/indents.toml | 4 +- runtime/queries/php/indents.toml | 2 +- runtime/queries/protobuf/indents.toml | 4 +- runtime/queries/python/indents.toml | 4 +- runtime/queries/ruby/indents.toml | 4 +- runtime/queries/rust/indents.toml | 4 +- runtime/queries/scala/indents.toml | 4 +- runtime/queries/svelte/indents.toml | 4 +- runtime/queries/tablegen/indents.toml | 4 +- runtime/queries/yaml/indents.toml | 2 +- runtime/queries/zig/indents.toml | 4 +- 27 files changed, 107 insertions(+), 58 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index b8f0d0a84961..1111e4a14e9f 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -267,39 +267,54 @@ fn get_first_in_line(mut node: Node, byte_pos: usize, new_line: bool) -> Vec(query_node: &IndentQueryNode, node: Node<'a>, cursor: &mut TreeCursor<'a>) -> bool { match query_node { IndentQueryNode::SimpleNode(_) => true, IndentQueryNode::ComplexNode { kind: _, - field_name, + kind_not_in, parent_kind_in, + field_name_in, } => { + if let Some(kind_not_in) = kind_not_in { + let kind = node.kind(); + if kind_not_in.iter().any(|k| k == kind) { + return false; + } + } if let Some(parent_kind_in) = parent_kind_in { let parent_matches = node.parent().map_or(false, |p| { - parent_kind_in.iter().any(|kind| kind.as_str() == p.kind()) + let parent_kind = p.kind(); + parent_kind_in + .iter() + .any(|kind| kind.as_str() == parent_kind) }); if !parent_matches { return false; } } - if let Some(field_name) = field_name { + if let Some(field_name_in) = field_name_in { let parent = match node.parent() { None => { return false; } Some(p) => p, }; - let mut found_child = false; - for child in parent.children_by_field_name(field_name, cursor) { - if child == node { - found_child = true; + cursor.reset(parent); + debug_assert!(cursor.goto_first_child()); + loop { + if cursor.node() == node { + if let Some(cursor_name) = cursor.field_name() { + if !field_name_in.iter().any(|n| n == cursor_name) { + return false; + } + } else { + return false; + } break; } - } - if !found_child { - return false; + debug_assert!(cursor.goto_next_sibling()); } } true @@ -525,6 +540,25 @@ mod test { really_long_variable_name_using_up_the_line |= really_long_fn_that_should_definitely_go_on_the_next_line(); + let ( + a_long_variable_name_in_this_tuple, + b_long_variable_name_in_this_tuple, + c_long_variable_name_in_this_tuple, + d_long_variable_name_in_this_tuple, + e_long_variable_name_in_this_tuple, + ): (usize, usize, usize, usize, usize) = + if really_long_fn_that_should_definitely_go_on_the_next_line() { + ( + 03294239434, + 1213412342314, + 21231234134, + 834534234549898789, + 9879234234543853457, + ) + } else { + (0, 1, 2, 3, 4) + }; + let test_function = function_with_param(this_param, that_param ); diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 08ecbf7ccb77..42a2832c763c 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -114,8 +114,9 @@ pub enum IndentQueryNode { // A node given by a list of characteristics which must all be fulfilled in order to match ComplexNode { kind: Option, - field_name: Option, + kind_not_in: Option>, parent_kind_in: Option>, + field_name_in: Option>, }, } impl IndentQueryNode { diff --git a/runtime/queries/c/indents.toml b/runtime/queries/c/indents.toml index f4076e171c10..4da7a05bd856 100644 --- a/runtime/queries/c/indents.toml +++ b/runtime/queries/c/indents.toml @@ -1,4 +1,10 @@ -indent = [ +[indent] +all = [ + # Indent non-grouped bodies of if-, while- and do-while-statements. + # TODO Add indent for the for-statement (ideally the body of a for-statement should have a field name in the tree-sitter grammar) + { kind_not_in = ["compound_statement"], parent_kind_in = ["if_statement", "while_statement", "do_statement"], field_name_in = ["consequence", "body"] } +] +tail = [ "compound_statement", "field_declaration_list", "enumerator_list", @@ -9,7 +15,8 @@ indent = [ "expression_statement", ] -outdent = [ +[outdent] +all = [ "case", "}", "]", diff --git a/runtime/queries/cmake/indents.toml b/runtime/queries/cmake/indents.toml index 8b886a4fb3dc..590a25551641 100644 --- a/runtime/queries/cmake/indents.toml +++ b/runtime/queries/cmake/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "if_condition", "foreach_loop", "while_loop", @@ -7,6 +7,6 @@ indent = [ "normal_command", ] -outdent = [ +outdent.all = [ ")" ] diff --git a/runtime/queries/cpp/indents.toml b/runtime/queries/cpp/indents.toml index 0ca2ed8bf7f7..46713c13ef8a 100644 --- a/runtime/queries/cpp/indents.toml +++ b/runtime/queries/cpp/indents.toml @@ -1,4 +1,10 @@ -indent = [ +[indent] +all = [ + # Indent non-grouped bodies of if-, while- and do-while-statements. + # TODO Add indent for the for-statement (ideally the body of a for-statement should have a field name in the tree-sitter grammar) + { kind_not_in = ["compound_statement"], parent_kind_in = ["if_statement", "while_statement", "do_statement"], field_name_in = ["consequence", "body"] } +] +tail = [ "compound_statement", "field_declaration_list", "enumerator_list", @@ -9,7 +15,8 @@ indent = [ "expression_statement", ] -outdent = [ +[outdent] +all = [ "case", "access_specifier", "}", diff --git a/runtime/queries/dart/indents.toml b/runtime/queries/dart/indents.toml index 5c11e05dd859..151dced156db 100644 --- a/runtime/queries/dart/indents.toml +++ b/runtime/queries/dart/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "class_body", "function_body", "function_expression_body", @@ -13,7 +13,7 @@ indent = [ "arguments" ] -outdent = [ +outdent.all = [ "}", "]", ")" diff --git a/runtime/queries/fish/indents.toml b/runtime/queries/fish/indents.toml index 6f1e563ae069..88fbfd3a33f2 100644 --- a/runtime/queries/fish/indents.toml +++ b/runtime/queries/fish/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "function_definition", "while_statement", "for_statement", @@ -7,6 +7,6 @@ indent = [ "switch_statement", ] -outdent = [ +outdent.all = [ "end" ] diff --git a/runtime/queries/glsl/indents.toml b/runtime/queries/glsl/indents.toml index a7fd499a9239..f7878baf40fd 100644 --- a/runtime/queries/glsl/indents.toml +++ b/runtime/queries/glsl/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "init_declarator", "compound_statement", "preproc_arg", @@ -10,7 +10,7 @@ indent = [ "compound_literal_expression" ] -outdent = [ +outdent.all = [ "#define", "#ifdef", "#endif", diff --git a/runtime/queries/javascript/indents.toml b/runtime/queries/javascript/indents.toml index 9d711ab23d4c..5415726d01d4 100644 --- a/runtime/queries/javascript/indents.toml +++ b/runtime/queries/javascript/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "array", "object", "arguments", @@ -21,7 +21,7 @@ indent = [ "object_type", ] -outdent = [ +outdent.all = [ "}", "]", ")" diff --git a/runtime/queries/json/indents.toml b/runtime/queries/json/indents.toml index 64a8d1757f09..80a91c345f24 100644 --- a/runtime/queries/json/indents.toml +++ b/runtime/queries/json/indents.toml @@ -1,9 +1,9 @@ -indent = [ +indent.tail = [ "object", "array" ] -outdent = [ +outdent.all = [ "]", "}" ] diff --git a/runtime/queries/llvm-mir-yaml/indents.toml b/runtime/queries/llvm-mir-yaml/indents.toml index ddc3578b1420..499994f7a190 100644 --- a/runtime/queries/llvm-mir-yaml/indents.toml +++ b/runtime/queries/llvm-mir-yaml/indents.toml @@ -1,3 +1,3 @@ -indent = [ +indent.tail = [ "block_mapping_pair", ] diff --git a/runtime/queries/llvm-mir/indents.toml b/runtime/queries/llvm-mir/indents.toml index 6a70e5adc2ab..f372dbb61df6 100644 --- a/runtime/queries/llvm-mir/indents.toml +++ b/runtime/queries/llvm-mir/indents.toml @@ -1,7 +1,7 @@ -indent = [ +indent.tail = [ "basic_block", ] -outdent = [ +outdent.all = [ "label", ] diff --git a/runtime/queries/llvm/indents.toml b/runtime/queries/llvm/indents.toml index 8cd603c8e668..ec2e2dfe293b 100644 --- a/runtime/queries/llvm/indents.toml +++ b/runtime/queries/llvm/indents.toml @@ -1,8 +1,8 @@ -indent = [ +indent.tail = [ "function_body", "instruction", ] -outdent = [ +outdent.all = [ "}", ] diff --git a/runtime/queries/lua/indents.toml b/runtime/queries/lua/indents.toml index df1a9752a043..e2cf429ede68 100644 --- a/runtime/queries/lua/indents.toml +++ b/runtime/queries/lua/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "function_definition", "variable_declaration", "local_variable_declaration", diff --git a/runtime/queries/nix/indents.toml b/runtime/queries/nix/indents.toml index b92ab752df99..1d5c487187c6 100644 --- a/runtime/queries/nix/indents.toml +++ b/runtime/queries/nix/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ # "function", "bind", "assert", @@ -12,7 +12,7 @@ indent = [ "parenthesized", ] -outdent = [ +outdent.all = [ "}", "]", ] diff --git a/runtime/queries/ocaml/indents.toml b/runtime/queries/ocaml/indents.toml index 7586b83a0f42..660c34df0e9a 100644 --- a/runtime/queries/ocaml/indents.toml +++ b/runtime/queries/ocaml/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "let_binding", "type_binding", "structure", @@ -8,6 +8,6 @@ indent = [ "match_case", ] -outdent = [ +outdent.all = [ "}", ] diff --git a/runtime/queries/perl/indents.toml b/runtime/queries/perl/indents.toml index 365e0663601d..6116b26b98c0 100644 --- a/runtime/queries/perl/indents.toml +++ b/runtime/queries/perl/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "function", "identifier", "method_invocation", @@ -12,6 +12,6 @@ indent = [ "word_list_qw" ] -outdent = [ +outdent.all = [ "}" ] diff --git a/runtime/queries/php/indents.toml b/runtime/queries/php/indents.toml index 85c104db55cd..2f30c6d1829c 100644 --- a/runtime/queries/php/indents.toml +++ b/runtime/queries/php/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "array_creation_expression", "arguments", "formal_parameters", diff --git a/runtime/queries/protobuf/indents.toml b/runtime/queries/protobuf/indents.toml index e655f8db6463..f0155d559524 100644 --- a/runtime/queries/protobuf/indents.toml +++ b/runtime/queries/protobuf/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "messageBody", "enumBody", "oneofBody", @@ -7,6 +7,6 @@ indent = [ "msgLit", ] -outdent = [ +outdent.all = [ "}", ] diff --git a/runtime/queries/python/indents.toml b/runtime/queries/python/indents.toml index 6bc684864bbe..a458c5715666 100644 --- a/runtime/queries/python/indents.toml +++ b/runtime/queries/python/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "list", "tuple", "dictionary", @@ -27,7 +27,7 @@ indent = [ "class_definition", ] -outdent = [ +outdent.all = [ ")", "]", "}", diff --git a/runtime/queries/ruby/indents.toml b/runtime/queries/ruby/indents.toml index b417751fc80c..5549739028e1 100644 --- a/runtime/queries/ruby/indents.toml +++ b/runtime/queries/ruby/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "argument_list", "array", "begin", @@ -16,7 +16,7 @@ indent = [ "singleton_method", ] -outdent = [ +outdent.all = [ ")", "}", "]", diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml index bdf0e10a99dd..07c5b8f45560 100644 --- a/runtime/queries/rust/indents.toml +++ b/runtime/queries/rust/indents.toml @@ -1,7 +1,7 @@ [indent] all = [ - { parent_kind_in = ["assignment_expression", "compound_assignment_expr"], field_name = "right"}, - { parent_kind_in = ["let_declaration"], field_name = "value" } + { parent_kind_in = ["assignment_expression", "compound_assignment_expr"], field_name_in = ["right"]}, + { parent_kind_in = ["let_declaration"], field_name_in = ["value"] } ] tail = [ "use_list", diff --git a/runtime/queries/scala/indents.toml b/runtime/queries/scala/indents.toml index 6de548442ab2..a34f0f2992ff 100644 --- a/runtime/queries/scala/indents.toml +++ b/runtime/queries/scala/indents.toml @@ -1,5 +1,5 @@ -indent = [ +indent.tail = [ "block", "arguments", "parameter", @@ -16,7 +16,7 @@ indent = [ "match_expression" ] -outdent = [ +outdent.all = [ "}", "]", ")" diff --git a/runtime/queries/svelte/indents.toml b/runtime/queries/svelte/indents.toml index 693db8e3dab2..b04247d7557e 100644 --- a/runtime/queries/svelte/indents.toml +++ b/runtime/queries/svelte/indents.toml @@ -1,11 +1,11 @@ -indent = [ +indent.tail = [ "element" "if_statement" "each_statement" "await_statement" ] -outdent = [ +outdent.all = [ "end_tag" "else_statement" "if_end_expr" diff --git a/runtime/queries/tablegen/indents.toml b/runtime/queries/tablegen/indents.toml index 43532f4d4ad6..abea12f2e54c 100644 --- a/runtime/queries/tablegen/indents.toml +++ b/runtime/queries/tablegen/indents.toml @@ -1,7 +1,7 @@ -indent = [ +indent.tail = [ "statement", ] -outdent = [ +outdent.all = [ "}", ] diff --git a/runtime/queries/yaml/indents.toml b/runtime/queries/yaml/indents.toml index ddc3578b1420..499994f7a190 100644 --- a/runtime/queries/yaml/indents.toml +++ b/runtime/queries/yaml/indents.toml @@ -1,3 +1,3 @@ -indent = [ +indent.tail = [ "block_mapping_pair", ] diff --git a/runtime/queries/zig/indents.toml b/runtime/queries/zig/indents.toml index 36ba8e55895c..234992dec957 100644 --- a/runtime/queries/zig/indents.toml +++ b/runtime/queries/zig/indents.toml @@ -1,4 +1,4 @@ -indent = [ +indent.tail = [ "Block", "BlockExpr", "ContainerDecl", @@ -9,7 +9,7 @@ indent = [ "InitList" ] -outdent = [ +outdent.all = [ "}", "]", ")" From d4c45332167cabee5600a8f935cf8091cd93838d Mon Sep 17 00:00:00 2001 From: Triton171 Date: Mon, 24 Jan 2022 18:46:19 +0100 Subject: [PATCH 05/19] Add comments & replace Option> with Vec<_> --- helix-core/src/indent.rs | 19 +++++++++++++------ helix-core/src/syntax.rs | 16 +++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 1111e4a14e9f..d0f7736d8d89 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -188,6 +188,8 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { len / tab_width } +/// The indent that is added for a single tree-sitter node/a single line. +/// This is different from the total indent ([IndentResult]) because multiple indents/outdents on the same line don't stack. struct AddedIndent { indent: bool, outdent: bool, @@ -205,7 +207,11 @@ impl AddedIndent { } } +/// The total indent for some line of code. +/// This is usually constructed by successively adding instances of [AddedIndent] struct IndentResult { + /// The total indent (the number of indent levels). + /// The string that this results in depends on the indent style (spaces or tabs, etc.) indent: i32, } impl IndentResult { @@ -277,13 +283,13 @@ fn matches<'a>(query_node: &IndentQueryNode, node: Node<'a>, cursor: &mut TreeCu parent_kind_in, field_name_in, } => { - if let Some(kind_not_in) = kind_not_in { + if !kind_not_in.is_empty() { let kind = node.kind(); if kind_not_in.iter().any(|k| k == kind) { return false; } } - if let Some(parent_kind_in) = parent_kind_in { + if !parent_kind_in.is_empty() { let parent_matches = node.parent().map_or(false, |p| { let parent_kind = p.kind(); parent_kind_in @@ -294,7 +300,7 @@ fn matches<'a>(query_node: &IndentQueryNode, node: Node<'a>, cursor: &mut TreeCu return false; } } - if let Some(field_name_in) = field_name_in { + if !field_name_in.is_empty() { let parent = match node.parent() { None => { return false; @@ -345,6 +351,7 @@ fn contains_match<'a>( false } +/// Returns whether the given scopes contain a match for this line and/or for the next fn scopes_contain_match<'a>( scopes: &IndentQueryScopes, node: Node<'a>, @@ -355,7 +362,7 @@ fn scopes_contain_match<'a>( (match_for_line, match_for_next) } -// The added indent for the line of the node and the next line +/// The added indent for the line of the node and the next line fn added_indent<'a>( query: &IndentQuery, node: Node<'a>, @@ -441,8 +448,8 @@ fn treesitter_indent_for_pos( Some(result.as_string(indent_style)) } -// Returns the indentation for a new line. -// This is done either using treesitter, or if that's not available by copying the indentation from the current line +/// Returns the indentation for a new line. +/// 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( language_config: Option<&LanguageConfiguration>, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 42a2832c763c..fd22a5859120 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -114,9 +114,15 @@ pub enum IndentQueryNode { // A node given by a list of characteristics which must all be fulfilled in order to match ComplexNode { kind: Option, - kind_not_in: Option>, - parent_kind_in: Option>, - field_name_in: Option>, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + kind_not_in: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + parent_kind_in: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + field_name_in: Vec, }, } impl IndentQueryNode { @@ -154,10 +160,6 @@ pub struct IndentQueryScopes { pub tail: Vec, } -impl IndentQueryScopes { - pub fn sort_nodes(&mut self) {} -} - impl<'de> Deserialize<'de> for IndentQueryScopes { fn deserialize(deserializer: D) -> Result where From 3f9166266eaab2a683a23c72ecfef8c454fb7aaf Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sat, 29 Jan 2022 18:09:08 +0100 Subject: [PATCH 06/19] Add more detailed documentation for tree-sitter indentation --- helix-core/src/indent.rs | 42 +++++++++++++++++++++++++++++++++++++++- helix-core/src/syntax.rs | 2 ++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index d0f7736d8d89..59bf1f35977c 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -201,6 +201,8 @@ impl AddedIndent { outdent: false, } } + /// Combine this [AddedIndent] with other. + /// This is intended for indents that apply to the same line. fn combine_with(&mut self, other: &AddedIndent) { self.indent |= other.indent; self.outdent |= other.outdent; @@ -218,6 +220,8 @@ impl IndentResult { fn new() -> Self { IndentResult { indent: 0 } } + /// Add the given [AddedIndent] to the [IndentResult]. + /// The [AddedIndent] should be the combination of all the added indents for one line. fn add(&mut self, added: &AddedIndent) { if added.indent && !added.outdent { self.indent += 1; @@ -378,6 +382,42 @@ fn added_indent<'a>( (line, next) } +/// Use the syntax tree to determine the indentation for a given position. +/// This can be used in 2 ways: +/// +/// - To get the correct indentation for an existing line (new_line=false), not necessarily equal to the current indentation. +/// - In this case, pos should be inside the first tree-sitter node on that line. +/// In most cases, this can just be the first non-whitespace on that line. +/// - To get the indentation for a new line (new_line=true). This behaves like the first usecase if the part of the current line +/// after pos were moved to a new line. +/// +/// The indentation is determined by traversing all the tree-sitter nodes containing the position. +/// Each of these nodes produces some [AddedIndent] for: +/// +/// - The line of the (beginning of the) node. This is defined by the scope `all` if this is the first node on its line. +/// - 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 [IndentResult] is simply the sum of the [AddedIndent] for all lines. +/// +/// Specifying which line exactly an [AddedIndent] applies to is important because indents on the same line combine differently than indents on different lines: +/// ```ignore +/// some_function(|| { +/// // Both the function parameters as well as the contained block should be indented. +/// // Because they are on the same line, this only yields one indent level +/// }); +/// ``` +/// +/// ```ignore +/// some_function( +/// parm1, +/// || { +/// // Here we get 2 indent levels because the 'parameters' and the 'block' node begin on different lines +/// }, +/// ); +/// ``` fn treesitter_indent_for_pos( query: &IndentQuery, syntax: &Syntax, @@ -681,7 +721,7 @@ where let line = text.line(i); if let Some(pos) = crate::find_first_non_whitespace_char(line) { let suggested_indent = treesitter_indent_for_pos( - &language_config.indent_query().unwrap(), + language_config.indent_query().unwrap(), &syntax, &IndentStyle::Spaces(4), text, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index fd22a5859120..a6880958b492 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -186,8 +186,10 @@ impl<'de> Deserialize<'de> for IndentQueryScopes { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct IndentQuery { + /// Every node specified here increases the indent level by one (in total at most one per line) #[serde(default)] pub indent: IndentQueryScopes, + /// Every node specified here decreases the indent level by one (in total at most one per line) #[serde(default)] pub outdent: IndentQueryScopes, } From 76663186693b9a653a0909665fb51dca9f7258b6 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sun, 30 Jan 2022 13:14:52 +0100 Subject: [PATCH 07/19] Improve code style in indent.rs --- helix-core/src/indent.rs | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 59bf1f35977c..f8aa7597eead 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -226,13 +226,11 @@ impl IndentResult { if added.indent && !added.outdent { self.indent += 1; } else if added.outdent && !added.indent { - self.indent = self.indent.saturating_sub(1); + self.indent -= 1; } } fn as_string(&self, indent_style: &IndentStyle) -> String { - indent_style - .as_str() - .repeat(std::cmp::max(self.indent, 0) as usize) + indent_style.as_str().repeat(0.max(self.indent) as usize) } } @@ -253,7 +251,7 @@ fn get_first_in_line(mut node: Node, byte_pos: usize, new_line: bool) -> Vec= 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 iff their parent is + // 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); } @@ -339,20 +337,17 @@ fn contains_match<'a>( ) -> bool { let current_kind = node.kind(); let first = scope.partition_point(|n| n.kind() < Some(current_kind)); - for named_node in scope[first..] + if scope[first..] .iter() .take_while(|n| n.kind() == Some(current_kind)) + .any(|named_node| matches(named_node, node, cursor)) { - if matches(named_node, node, cursor) { - return true; - } - } - for unnamed_node in scope.iter().take_while(|n| n.kind().is_none()) { - if matches(unnamed_node, node, cursor) { - return true; - } + return true; } - false + scope + .iter() + .take_while(|n| n.kind().is_none()) + .any(|unnamed_node| matches(unnamed_node, node, cursor)) } /// Returns whether the given scopes contain a match for this line and/or for the next @@ -429,12 +424,7 @@ fn treesitter_indent_for_pos( ) -> Option { let mut cursor = syntax.tree().walk(); let byte_pos = text.char_to_byte(pos); - let mut node = match get_lowest_node(syntax.tree().root_node(), query, byte_pos) { - Some(n) => n, - None => { - return None; - } - }; + let mut node = get_lowest_node(syntax.tree().root_node(), query, byte_pos)?; let mut first_in_line = get_first_in_line(node, byte_pos, new_line); let mut result = IndentResult::new(); From 5a8517b75f7acbaeef46917b081e62b3d8a315cf Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sat, 12 Feb 2022 22:27:40 +0100 Subject: [PATCH 08/19] Use tree-sitter queries for indentation instead of TOML config. Migrate existing indent queries. --- helix-core/src/indent.rs | 312 +++++++++++---------- helix-core/src/syntax.rs | 106 +------ runtime/queries/c/indents.scm | 35 +++ runtime/queries/c/indents.toml | 23 -- runtime/queries/cmake/indents.scm | 10 + runtime/queries/cmake/indents.toml | 12 - runtime/queries/cpp/indents.scm | 3 + runtime/queries/cpp/indents.toml | 24 -- runtime/queries/dart/indents.scm | 20 ++ runtime/queries/dart/indents.toml | 20 -- runtime/queries/fish/indents.scm | 12 + runtime/queries/fish/indents.toml | 12 - runtime/queries/glsl/indents.scm | 19 ++ runtime/queries/glsl/indents.toml | 19 -- runtime/queries/go/indents.scm | 26 ++ runtime/queries/go/indents.toml | 32 --- runtime/queries/javascript/indents.scm | 22 ++ runtime/queries/javascript/indents.toml | 28 -- runtime/queries/json/indents.scm | 9 + runtime/queries/json/indents.toml | 9 - runtime/queries/llvm-mir-yaml/indents.scm | 2 + runtime/queries/llvm-mir-yaml/indents.toml | 3 - runtime/queries/llvm-mir/indents.scm | 3 + runtime/queries/llvm-mir/indents.toml | 7 - runtime/queries/llvm/indents.scm | 6 + runtime/queries/llvm/indents.toml | 8 - runtime/queries/lua/indents.scm | 24 ++ runtime/queries/lua/indents.toml | 24 -- runtime/queries/nix/indents.scm | 18 ++ runtime/queries/nix/indents.toml | 18 -- runtime/queries/ocaml/indents.scm | 12 + runtime/queries/ocaml/indents.toml | 13 - runtime/queries/perl/indents.scm | 15 + runtime/queries/perl/indents.toml | 17 -- runtime/queries/php/indents.scm | 17 ++ runtime/queries/php/indents.toml | 17 -- runtime/queries/protobuf/indents.scm | 11 + runtime/queries/protobuf/indents.toml | 12 - runtime/queries/python/indents.scm | 38 +++ runtime/queries/python/indents.toml | 39 --- runtime/queries/ruby/indents.scm | 25 ++ runtime/queries/ruby/indents.toml | 25 -- runtime/queries/rust/indents.scm | 38 +++ runtime/queries/rust/indents.toml | 34 --- runtime/queries/scala/indents.scm | 22 ++ runtime/queries/scala/indents.toml | 23 -- runtime/queries/svelte/indents.scm | 17 ++ runtime/queries/svelte/indents.toml | 18 -- runtime/queries/tablegen/indents.scm | 3 + runtime/queries/tablegen/indents.toml | 7 - runtime/queries/typescript/indents.scm | 7 + runtime/queries/typescript/indents.toml | 1 - runtime/queries/yaml/indents.scm | 2 + runtime/queries/yaml/indents.toml | 3 - runtime/queries/zig/indents.scm | 16 ++ runtime/queries/zig/indents.toml | 16 -- 56 files changed, 601 insertions(+), 713 deletions(-) create mode 100644 runtime/queries/c/indents.scm delete mode 100644 runtime/queries/c/indents.toml create mode 100644 runtime/queries/cmake/indents.scm delete mode 100644 runtime/queries/cmake/indents.toml create mode 100644 runtime/queries/cpp/indents.scm delete mode 100644 runtime/queries/cpp/indents.toml create mode 100644 runtime/queries/dart/indents.scm delete mode 100644 runtime/queries/dart/indents.toml create mode 100644 runtime/queries/fish/indents.scm delete mode 100644 runtime/queries/fish/indents.toml create mode 100644 runtime/queries/glsl/indents.scm delete mode 100644 runtime/queries/glsl/indents.toml create mode 100644 runtime/queries/go/indents.scm delete mode 100644 runtime/queries/go/indents.toml create mode 100644 runtime/queries/javascript/indents.scm delete mode 100644 runtime/queries/javascript/indents.toml create mode 100644 runtime/queries/json/indents.scm delete mode 100644 runtime/queries/json/indents.toml create mode 100644 runtime/queries/llvm-mir-yaml/indents.scm delete mode 100644 runtime/queries/llvm-mir-yaml/indents.toml create mode 100644 runtime/queries/llvm-mir/indents.scm delete mode 100644 runtime/queries/llvm-mir/indents.toml create mode 100644 runtime/queries/llvm/indents.scm delete mode 100644 runtime/queries/llvm/indents.toml create mode 100644 runtime/queries/lua/indents.scm delete mode 100644 runtime/queries/lua/indents.toml create mode 100644 runtime/queries/nix/indents.scm delete mode 100644 runtime/queries/nix/indents.toml create mode 100644 runtime/queries/ocaml/indents.scm delete mode 100644 runtime/queries/ocaml/indents.toml create mode 100644 runtime/queries/perl/indents.scm delete mode 100644 runtime/queries/perl/indents.toml create mode 100644 runtime/queries/php/indents.scm delete mode 100644 runtime/queries/php/indents.toml create mode 100644 runtime/queries/protobuf/indents.scm delete mode 100644 runtime/queries/protobuf/indents.toml create mode 100644 runtime/queries/python/indents.scm delete mode 100644 runtime/queries/python/indents.toml create mode 100644 runtime/queries/ruby/indents.scm delete mode 100644 runtime/queries/ruby/indents.toml create mode 100644 runtime/queries/rust/indents.scm delete mode 100644 runtime/queries/rust/indents.toml create mode 100644 runtime/queries/scala/indents.scm delete mode 100644 runtime/queries/scala/indents.toml create mode 100644 runtime/queries/svelte/indents.scm delete mode 100644 runtime/queries/svelte/indents.toml create mode 100644 runtime/queries/tablegen/indents.scm delete mode 100644 runtime/queries/tablegen/indents.toml create mode 100644 runtime/queries/typescript/indents.scm delete mode 120000 runtime/queries/typescript/indents.toml create mode 100644 runtime/queries/yaml/indents.scm delete mode 100644 runtime/queries/yaml/indents.toml create mode 100644 runtime/queries/zig/indents.scm delete mode 100644 runtime/queries/zig/indents.toml diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index f8aa7597eead..be9a27ead391 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,8 +1,10 @@ -use tree_sitter::TreeCursor; +use std::collections::{hash_map::Entry, HashMap}; + +use tree_sitter::{Query, QueryCursor}; use crate::{ chars::{char_is_line_ending, char_is_whitespace}, - syntax::{IndentQuery, IndentQueryNode, IndentQueryScopes, LanguageConfiguration, Syntax}, + syntax::{LanguageConfiguration, RopeProvider, Syntax}, tree_sitter::Node, Rope, RopeSlice, }; @@ -188,25 +190,39 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { len / tab_width } -/// The indent that is added for a single tree-sitter node/a single line. -/// This is different from the total indent ([IndentResult]) because multiple indents/outdents on the same line don't stack. -struct AddedIndent { - indent: bool, - outdent: bool, -} -impl AddedIndent { - fn new() -> Self { - AddedIndent { - indent: false, - outdent: 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, byte_pos: usize, new_line: bool) -> Vec { + 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 && 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(parent) = node.parent() { + node = parent; + } else { + break; } } - /// Combine this [AddedIndent] with other. - /// This is intended for indents that apply to the same line. - fn combine_with(&mut self, other: &AddedIndent) { - self.indent |= other.indent; - self.outdent |= other.outdent; + + 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. @@ -234,147 +250,126 @@ impl IndentResult { } } -// Get the node where to start the indent query (this is usually just the lowest node containing byte_pos) -fn get_lowest_node<'a>(root: Node<'a>, _query: &IndentQuery, byte_pos: usize) -> Option> { - root.descendant_for_byte_range(byte_pos, byte_pos) - // TODO Special handling for languages like python +/// The indent that is added for a single line. +/// This is different from the total indent ([IndentResult]) because multiple indents/outdents on the same line don't stack. +/// It should be constructed by successively adding the relevant indent captures. +struct AddedIndent { + indent: bool, + outdent: bool, } - -// 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, byte_pos: usize, new_line: bool) -> Vec { - 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 && 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); +impl AddedIndent { + /// Returns the [AddedIndent] for a line without any indent definitions + fn new() -> Self { + AddedIndent { + indent: false, + outdent: false, } - if let Some(parent) = node.parent() { - node = parent; - } else { - break; + } + /// Adds an indent capture to this indent. + fn add_capture(&mut self, capture: &IndentCaptureType) { + match capture { + IndentCaptureType::Indent => { + self.indent = true; + } + IndentCaptureType::Outdent => { + self.outdent = true; + } } } +} - 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); +/// An indent definition which corresponds to a capture from the indent query +struct IndentCapture { + capture_type: IndentCaptureType, + scope: IndentScope, +} +enum IndentCaptureType { + Indent, + Outdent, +} +impl IndentCaptureType { + fn default_scope(&self) -> IndentScope { + match *self { + IndentCaptureType::Indent => IndentScope::Tail, + IndentCaptureType::Outdent => IndentScope::All, } } - result +} +/// This defines which part of a node an [IndentCapture] applies to. +/// Each [IndentCaptureType] has a default scope, but the scope can be changed +/// with `#set!` property declarations. +enum IndentScope { + /// The indent applies to the whole node + All, + /// The indent applies to everything except for the first line of the node + Tail, } -// This assumes that the kind matches and checks for all the other conditions -fn matches<'a>(query_node: &IndentQueryNode, node: Node<'a>, cursor: &mut TreeCursor<'a>) -> bool { - match query_node { - IndentQueryNode::SimpleNode(_) => true, - IndentQueryNode::ComplexNode { - kind: _, - kind_not_in, - parent_kind_in, - field_name_in, - } => { - if !kind_not_in.is_empty() { - let kind = node.kind(); - if kind_not_in.iter().any(|k| k == kind) { - return false; - } - } - if !parent_kind_in.is_empty() { - let parent_matches = node.parent().map_or(false, |p| { - let parent_kind = p.kind(); - parent_kind_in - .iter() - .any(|kind| kind.as_str() == parent_kind) - }); - if !parent_matches { - return false; +/// Execute the indent query. +/// Returns for each node (identified by its id) a list of indent captures for that node. +fn query_indents( + query: &Query, + syntax: &Syntax, + text: RopeSlice, + byte_pos: usize, +) -> HashMap> { + // TODO Maybe reuse this cursor? + let mut cursor = QueryCursor::new(); + let mut indent_captures: HashMap> = HashMap::new(); + cursor.set_byte_range(byte_pos..byte_pos + 1); + // Iterate over all captures from the query + for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) { + for capture in m.captures { + let capture_type = query.capture_names()[capture.index as usize].as_str(); + let capture_type = match capture_type { + "indent" => IndentCaptureType::Indent, + "outdent" => IndentCaptureType::Outdent, + _ => { + // Ignore any unknown captures (these may be needed for predicates such as #match?) + continue; } - } - if !field_name_in.is_empty() { - let parent = match node.parent() { - None => { - return false; - } - Some(p) => p, - }; - cursor.reset(parent); - debug_assert!(cursor.goto_first_child()); - loop { - if cursor.node() == node { - if let Some(cursor_name) = cursor.field_name() { - if !field_name_in.iter().any(|n| n == cursor_name) { - return false; + }; + let scope = capture_type.default_scope(); + let mut indent_capture = IndentCapture { + capture_type, + scope, + }; + // 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_ref().map(|s| s.as_ref()) { + 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" + ); } - } else { - return false; } - break; } - debug_assert!(cursor.goto_next_sibling()); + _ => { + panic!( + "Invalid indent query: Unknown property \"{}\"", + property.key + ); + } + } + } + match indent_captures.entry(capture.node.id()) { + Entry::Occupied(mut e) => { + e.get_mut().push(indent_capture); + } + Entry::Vacant(e) => { + e.insert(vec![indent_capture]); } } - true } } -} - -fn contains_match<'a>( - scope: &[IndentQueryNode], - node: Node<'a>, - cursor: &mut TreeCursor<'a>, -) -> bool { - let current_kind = node.kind(); - let first = scope.partition_point(|n| n.kind() < Some(current_kind)); - if scope[first..] - .iter() - .take_while(|n| n.kind() == Some(current_kind)) - .any(|named_node| matches(named_node, node, cursor)) - { - return true; - } - scope - .iter() - .take_while(|n| n.kind().is_none()) - .any(|unnamed_node| matches(unnamed_node, node, cursor)) -} - -/// Returns whether the given scopes contain a match for this line and/or for the next -fn scopes_contain_match<'a>( - scopes: &IndentQueryScopes, - node: Node<'a>, - cursor: &mut TreeCursor<'a>, -) -> (bool, bool) { - let match_for_line = contains_match(&scopes.all, node, cursor); - let match_for_next = contains_match(&scopes.tail, node, cursor); - (match_for_line, match_for_next) -} - -/// The added indent for the line of the node and the next line -fn added_indent<'a>( - query: &IndentQuery, - node: Node<'a>, - cursor: &mut TreeCursor<'a>, -) -> (AddedIndent, AddedIndent) { - let (indent, next_indent) = scopes_contain_match(&query.indent, node, cursor); - let (outdent, next_outdent) = scopes_contain_match(&query.outdent, node, cursor); - let line = AddedIndent { indent, outdent }; - let next = AddedIndent { - indent: next_indent, - outdent: next_outdent, - }; - (line, next) + indent_captures } /// Use the syntax tree to determine the indentation for a given position. @@ -414,7 +409,7 @@ fn added_indent<'a>( /// ); /// ``` fn treesitter_indent_for_pos( - query: &IndentQuery, + query: &Query, syntax: &Syntax, indent_style: &IndentStyle, text: RopeSlice, @@ -422,10 +417,13 @@ fn treesitter_indent_for_pos( pos: usize, new_line: bool, ) -> Option { - let mut cursor = syntax.tree().walk(); let byte_pos = text.char_to_byte(pos); - let mut node = get_lowest_node(syntax.tree().root_node(), query, byte_pos)?; + let mut node = syntax + .tree() + .root_node() + .descendant_for_byte_range(byte_pos, byte_pos)?; let mut first_in_line = get_first_in_line(node, byte_pos, new_line); + let query_result = query_indents(query, syntax, text, byte_pos); let mut result = IndentResult::new(); // We always keep track of all the indent changes on one line, in order to only indent once @@ -433,13 +431,24 @@ fn treesitter_indent_for_pos( let mut indent_for_line = AddedIndent::new(); let mut indent_for_line_below = AddedIndent::new(); loop { - let node_indents = added_indent(query, node, &mut cursor); - if *first_in_line.last().unwrap() { - indent_for_line.combine_with(&node_indents.0); - } else { - indent_for_line_below.combine_with(&node_indents.0); + let is_first = *first_in_line.last().unwrap(); + // Apply all indent definitions for this node + if let Some(definitions) = query_result.get(&node.id()) { + for definition in definitions { + match definition.scope { + IndentScope::All => { + if is_first { + indent_for_line.add_capture(&definition.capture_type); + } else { + indent_for_line_below.add_capture(&definition.capture_type); + } + } + IndentScope::Tail => { + indent_for_line_below.add_capture(&definition.capture_type); + } + } + } } - indent_for_line_below.combine_with(&node_indents.1); if let Some(parent) = node.parent() { let mut node_line = node.start_position().row; @@ -507,7 +516,6 @@ pub fn indent_for_newline( return indent; }; } - let indent_level = indent_level_for_line(text.line(current_line), tab_width); indent_style.as_str().repeat(indent_level) } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index a6880958b492..a17d0c0a3a3d 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -84,7 +84,7 @@ pub struct LanguageConfiguration { pub indent: Option, #[serde(skip)] - pub(crate) indent_query: OnceCell>, + pub(crate) indent_query: OnceCell>, #[serde(skip)] pub(crate) textobject_query: OnceCell>, } @@ -106,94 +106,6 @@ pub struct IndentationConfiguration { pub unit: String, } -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum IndentQueryNode { - // A node given just by its kind - SimpleNode(String), - // A node given by a list of characteristics which must all be fulfilled in order to match - ComplexNode { - kind: Option, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - kind_not_in: Vec, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - parent_kind_in: Vec, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - field_name_in: Vec, - }, -} -impl IndentQueryNode { - pub fn kind(&self) -> Option<&str> { - match self { - IndentQueryNode::SimpleNode(n) => Some(n), - IndentQueryNode::ComplexNode { kind, .. } => kind.as_ref().map(|k| k.as_str()), - } - } -} -impl PartialEq for IndentQueryNode { - fn eq(&self, other: &Self) -> bool { - self.kind().eq(&other.kind()) - } -} -impl Eq for IndentQueryNode {} -impl PartialOrd for IndentQueryNode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Ord for IndentQueryNode { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.kind().cmp(&other.kind()) - } -} - -#[derive(Default, Debug, Serialize)] -pub struct IndentQueryScopes { - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub all: Vec, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub tail: Vec, -} - -impl<'de> Deserialize<'de> for IndentQueryScopes { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Derived { - #[serde(default)] - pub all: Vec, - #[serde(default)] - pub tail: Vec, - } - let derived = Derived::deserialize(deserializer)?; - let mut result = IndentQueryScopes { - all: derived.all, - tail: derived.tail, - }; - result.all.sort_unstable(); - result.tail.sort_unstable(); - Ok(result) - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct IndentQuery { - /// Every node specified here increases the indent level by one (in total at most one per line) - #[serde(default)] - pub indent: IndentQueryScopes, - /// Every node specified here decreases the indent level by one (in total at most one per line) - #[serde(default)] - pub outdent: IndentQueryScopes, -} - #[derive(Debug)] pub struct TextObjectQuery { pub query: Query, @@ -310,13 +222,13 @@ impl LanguageConfiguration { self.highlight_config.get().is_some() } - pub fn indent_query(&self) -> Option<&IndentQuery> { + pub fn indent_query(&self) -> Option<&Query> { self.indent_query .get_or_init(|| { - let language = self.language_id.to_ascii_lowercase(); - - let toml = load_runtime_file(&language, "indents.toml").ok()?; - toml::from_slice(toml.as_bytes()).ok() + let lang_name = self.language_id.to_ascii_lowercase(); + let query_text = read_query(&lang_name, "indents.scm"); + let lang = self.highlight_config.get()?.as_ref()?.language; + Query::new(lang, &query_text).ok() }) .as_ref() } @@ -1088,7 +1000,7 @@ struct HighlightIter<'a> { } // Adapter to convert rope chunks to bytes -struct ChunksBytes<'a> { +pub struct ChunksBytes<'a> { chunks: ropey::iter::Chunks<'a>, } impl<'a> Iterator for ChunksBytes<'a> { @@ -1098,7 +1010,7 @@ impl<'a> Iterator for ChunksBytes<'a> { } } -struct RopeProvider<'a>(RopeSlice<'a>); +pub struct RopeProvider<'a>(pub RopeSlice<'a>); impl<'a> TextProvider<'a> for RopeProvider<'a> { type I = ChunksBytes<'a>; @@ -1996,7 +1908,7 @@ mod test { #[test] fn test_load_runtime_file() { // Test to make sure we can load some data from the runtime directory. - let contents = load_runtime_file("rust", "indents.toml").unwrap(); + let contents = load_runtime_file("rust", "indents.scm").unwrap(); assert!(!contents.is_empty()); let results = load_runtime_file("rust", "does-not-exist"); diff --git a/runtime/queries/c/indents.scm b/runtime/queries/c/indents.scm new file mode 100644 index 000000000000..b080daf50743 --- /dev/null +++ b/runtime/queries/c/indents.scm @@ -0,0 +1,35 @@ +[ + (compound_statement) + (field_declaration_list) + (enumerator_list) + (parameter_list) + (init_declarator) + (case_statement) + (expression_statement) +] @indent + +[ + "case" + "}" + "]" +] @outdent + +; The #not-match? is just required to exclude compound statements. +; It would be nice to do this somehow without regexes +(if_statement + consequence: (_) @indent + (#not-match? @indent "\\\{*\\\}") + (#set! "scope" "all")) +(while_statement + body: (_) @indent + (#not-match? @indent "\\\{*\\\}") + (#set! "scope" "all")) +(do_statement + body: (_) @indent + (#not-match? @indent "\\\{*\\\}") + (#set! "scope" "all")) +(for_statement + ")" + (_) @indent + (#not-match? @indent "\\\{*\\\}") + (#set! "scope" "all")) diff --git a/runtime/queries/c/indents.toml b/runtime/queries/c/indents.toml deleted file mode 100644 index 4da7a05bd856..000000000000 --- a/runtime/queries/c/indents.toml +++ /dev/null @@ -1,23 +0,0 @@ -[indent] -all = [ - # Indent non-grouped bodies of if-, while- and do-while-statements. - # TODO Add indent for the for-statement (ideally the body of a for-statement should have a field name in the tree-sitter grammar) - { kind_not_in = ["compound_statement"], parent_kind_in = ["if_statement", "while_statement", "do_statement"], field_name_in = ["consequence", "body"] } -] -tail = [ - "compound_statement", - "field_declaration_list", - "enumerator_list", - "parameter_list", - "init_declarator", - "case_statement", - "condition_clause", - "expression_statement", -] - -[outdent] -all = [ - "case", - "}", - "]", -] diff --git a/runtime/queries/cmake/indents.scm b/runtime/queries/cmake/indents.scm new file mode 100644 index 000000000000..199b1031e5c6 --- /dev/null +++ b/runtime/queries/cmake/indents.scm @@ -0,0 +1,10 @@ +[ + (if_condition) + (foreach_loop) + (while_loop) + (function_def) + (macro_def) + (normal_command) +] @indent + +")" @outdent diff --git a/runtime/queries/cmake/indents.toml b/runtime/queries/cmake/indents.toml deleted file mode 100644 index 590a25551641..000000000000 --- a/runtime/queries/cmake/indents.toml +++ /dev/null @@ -1,12 +0,0 @@ -indent.tail = [ - "if_condition", - "foreach_loop", - "while_loop", - "function_def", - "macro_def", - "normal_command", -] - -outdent.all = [ - ")" -] diff --git a/runtime/queries/cpp/indents.scm b/runtime/queries/cpp/indents.scm new file mode 100644 index 000000000000..36876f9490d7 --- /dev/null +++ b/runtime/queries/cpp/indents.scm @@ -0,0 +1,3 @@ +; inherits: c + +(access_specifier) @outdent diff --git a/runtime/queries/cpp/indents.toml b/runtime/queries/cpp/indents.toml deleted file mode 100644 index 46713c13ef8a..000000000000 --- a/runtime/queries/cpp/indents.toml +++ /dev/null @@ -1,24 +0,0 @@ -[indent] -all = [ - # Indent non-grouped bodies of if-, while- and do-while-statements. - # TODO Add indent for the for-statement (ideally the body of a for-statement should have a field name in the tree-sitter grammar) - { kind_not_in = ["compound_statement"], parent_kind_in = ["if_statement", "while_statement", "do_statement"], field_name_in = ["consequence", "body"] } -] -tail = [ - "compound_statement", - "field_declaration_list", - "enumerator_list", - "parameter_list", - "init_declarator", - "case_statement", - "condition_clause", - "expression_statement", -] - -[outdent] -all = [ - "case", - "access_specifier", - "}", - "]", -] diff --git a/runtime/queries/dart/indents.scm b/runtime/queries/dart/indents.scm new file mode 100644 index 000000000000..af94ec0c471e --- /dev/null +++ b/runtime/queries/dart/indents.scm @@ -0,0 +1,20 @@ +[ + (class_body) + (function_body) + (function_expression_body) + (declaration) + (initializers) + (switch_block) + (if_statement) + (formal_parameter_list) + (formal_parameter) + (list_literal) + (return_statement) + (arguments) +] @indent + +[ + "}" + "]" + ")" +] @outdent diff --git a/runtime/queries/dart/indents.toml b/runtime/queries/dart/indents.toml deleted file mode 100644 index 151dced156db..000000000000 --- a/runtime/queries/dart/indents.toml +++ /dev/null @@ -1,20 +0,0 @@ -indent.tail = [ - "class_body", - "function_body", - "function_expression_body", - "declaration", - "initializers", - "switch_block", - "if_statement", - "formal_parameter_list", - "formal_parameter", - "list_literal", - "return_statement", - "arguments" -] - -outdent.all = [ - "}", - "]", - ")" -] diff --git a/runtime/queries/fish/indents.scm b/runtime/queries/fish/indents.scm new file mode 100644 index 000000000000..ba7c65eade46 --- /dev/null +++ b/runtime/queries/fish/indents.scm @@ -0,0 +1,12 @@ +[ + (function_definition) + (while_statement) + (for_statement) + (if_statement) + (begin_statement) + (switch_statement) +] @indent + +[ + "end" +] @outdent diff --git a/runtime/queries/fish/indents.toml b/runtime/queries/fish/indents.toml deleted file mode 100644 index 88fbfd3a33f2..000000000000 --- a/runtime/queries/fish/indents.toml +++ /dev/null @@ -1,12 +0,0 @@ -indent.tail = [ - "function_definition", - "while_statement", - "for_statement", - "if_statement", - "begin_statement", - "switch_statement", -] - -outdent.all = [ - "end" -] diff --git a/runtime/queries/glsl/indents.scm b/runtime/queries/glsl/indents.scm new file mode 100644 index 000000000000..a8b55a424734 --- /dev/null +++ b/runtime/queries/glsl/indents.scm @@ -0,0 +1,19 @@ +[ + (init_declarator) + (compound_statement) + (preproc_arg) + (field_declaration_list) + (case_statement) + (conditional_expression) + (enumerator_list) + (struct_specifier) + (compound_literal_expression) +] @indent + +[ + "#define" + "#ifdef" + "#endif" + "{" + "}" +] @outdent diff --git a/runtime/queries/glsl/indents.toml b/runtime/queries/glsl/indents.toml deleted file mode 100644 index f7878baf40fd..000000000000 --- a/runtime/queries/glsl/indents.toml +++ /dev/null @@ -1,19 +0,0 @@ -indent.tail = [ - "init_declarator", - "compound_statement", - "preproc_arg", - "field_declaration_list", - "case_statement", - "conditional_expression", - "enumerator_list", - "struct_specifier", - "compound_literal_expression" -] - -outdent.all = [ - "#define", - "#ifdef", - "#endif", - "{", - "}" -] diff --git a/runtime/queries/go/indents.scm b/runtime/queries/go/indents.scm new file mode 100644 index 000000000000..881db4c2750e --- /dev/null +++ b/runtime/queries/go/indents.scm @@ -0,0 +1,26 @@ +[ + (import_declaration) + (const_declaration) + (type_declaration) + (type_spec) + (func_literal) + (literal_value) + (element) + (keyed_element) + (expression_case) + (default_case) + (type_case) + (communication_case) + (argument_list) + (field_declaration_list) + (block) + (type_switch_statement) + (expression_switch_statement) +] @indent + +[ + "case" + "}" + "]" + ")" +] @outdent diff --git a/runtime/queries/go/indents.toml b/runtime/queries/go/indents.toml deleted file mode 100644 index cd0d5465c6ee..000000000000 --- a/runtime/queries/go/indents.toml +++ /dev/null @@ -1,32 +0,0 @@ -indent.tail = [ - "import_declaration", - "const_declaration", - #"var_declaration", - #"short_var_declaration", - "type_declaration", - "type_spec", - # simply block should be enough - # "function_declaration", - # "method_declaration", - # "composite_literal", - "func_literal", - "literal_value", - "element", - "keyed_element", - "expression_case", - "default_case", - "type_case", - "communication_case", - "argument_list", - "field_declaration_list", - "block", - "type_switch_statement", - "expression_switch_statement" -] - -outdent.all = [ - "case", - "}", - "]", - ")" -] diff --git a/runtime/queries/javascript/indents.scm b/runtime/queries/javascript/indents.scm new file mode 100644 index 000000000000..539d54549b34 --- /dev/null +++ b/runtime/queries/javascript/indents.scm @@ -0,0 +1,22 @@ +[ + (array) + (object) + (arguments) + (formal_parameters) + + (statement_block) + (object_pattern) + (class_body) + (named_imports) + + (binary_expression) + (return_statement) + (template_substitution) + (export_clause) +] @indent + +[ + "}" + "]" + ")" +] @outdent diff --git a/runtime/queries/javascript/indents.toml b/runtime/queries/javascript/indents.toml deleted file mode 100644 index 5415726d01d4..000000000000 --- a/runtime/queries/javascript/indents.toml +++ /dev/null @@ -1,28 +0,0 @@ -indent.tail = [ - "array", - "object", - "arguments", - "formal_parameters", - - "statement_block", - "object_pattern", - "class_body", - "named_imports", - - "binary_expression", - "return_statement", - "template_substitution", - # (expression_statement (call_expression)) - "export_clause", - - # typescript - "enum_declaration", - "interface_declaration", - "object_type", -] - -outdent.all = [ - "}", - "]", - ")" -] diff --git a/runtime/queries/json/indents.scm b/runtime/queries/json/indents.scm new file mode 100644 index 000000000000..f756e609acfe --- /dev/null +++ b/runtime/queries/json/indents.scm @@ -0,0 +1,9 @@ +[ + (object) + (array) +] @indent + +[ + "]" + "}" +] @outdent diff --git a/runtime/queries/json/indents.toml b/runtime/queries/json/indents.toml deleted file mode 100644 index 80a91c345f24..000000000000 --- a/runtime/queries/json/indents.toml +++ /dev/null @@ -1,9 +0,0 @@ -indent.tail = [ - "object", - "array" -] - -outdent.all = [ - "]", - "}" -] diff --git a/runtime/queries/llvm-mir-yaml/indents.scm b/runtime/queries/llvm-mir-yaml/indents.scm new file mode 100644 index 000000000000..70a00b695389 --- /dev/null +++ b/runtime/queries/llvm-mir-yaml/indents.scm @@ -0,0 +1,2 @@ +(block_mapping_pair) @indent + diff --git a/runtime/queries/llvm-mir-yaml/indents.toml b/runtime/queries/llvm-mir-yaml/indents.toml deleted file mode 100644 index 499994f7a190..000000000000 --- a/runtime/queries/llvm-mir-yaml/indents.toml +++ /dev/null @@ -1,3 +0,0 @@ -indent.tail = [ - "block_mapping_pair", -] diff --git a/runtime/queries/llvm-mir/indents.scm b/runtime/queries/llvm-mir/indents.scm new file mode 100644 index 000000000000..12c86268cb00 --- /dev/null +++ b/runtime/queries/llvm-mir/indents.scm @@ -0,0 +1,3 @@ +(basic_block) @indent + +(label) @outdent diff --git a/runtime/queries/llvm-mir/indents.toml b/runtime/queries/llvm-mir/indents.toml deleted file mode 100644 index f372dbb61df6..000000000000 --- a/runtime/queries/llvm-mir/indents.toml +++ /dev/null @@ -1,7 +0,0 @@ -indent.tail = [ - "basic_block", -] - -outdent.all = [ - "label", -] diff --git a/runtime/queries/llvm/indents.scm b/runtime/queries/llvm/indents.scm new file mode 100644 index 000000000000..7065bee19a42 --- /dev/null +++ b/runtime/queries/llvm/indents.scm @@ -0,0 +1,6 @@ +[ + (function_body) + (instruction) +] @indent + +"}" @outdent diff --git a/runtime/queries/llvm/indents.toml b/runtime/queries/llvm/indents.toml deleted file mode 100644 index ec2e2dfe293b..000000000000 --- a/runtime/queries/llvm/indents.toml +++ /dev/null @@ -1,8 +0,0 @@ -indent.tail = [ - "function_body", - "instruction", -] - -outdent.all = [ - "}", -] diff --git a/runtime/queries/lua/indents.scm b/runtime/queries/lua/indents.scm new file mode 100644 index 000000000000..55a812c51ba8 --- /dev/null +++ b/runtime/queries/lua/indents.scm @@ -0,0 +1,24 @@ +[ + (function_definition) + (variable_declaration) + (local_variable_declaration) + (field) + (local_function) + (function) + (if_statement) + (for_statement) + (for_in_statement) + (repeat_statement) + (return_statement) + (while_statement) + (table) + (arguments) + (do_statement) +] @indent + +[ + "end" + "until" + "}" + ")" +] @outdent diff --git a/runtime/queries/lua/indents.toml b/runtime/queries/lua/indents.toml deleted file mode 100644 index e2cf429ede68..000000000000 --- a/runtime/queries/lua/indents.toml +++ /dev/null @@ -1,24 +0,0 @@ -indent.tail = [ - "function_definition", - "variable_declaration", - "local_variable_declaration", - "field", - "local_function", - "function", - "if_statement", - "for_statement", - "for_in_statement", - "repeat_statement", - "return_statement", - "while_statement", - "table", - "arguments", - "do_statement", -] - -oudent = [ - "end", - "until", - "}", - ")", -] diff --git a/runtime/queries/nix/indents.scm b/runtime/queries/nix/indents.scm new file mode 100644 index 000000000000..06e811408dd8 --- /dev/null +++ b/runtime/queries/nix/indents.scm @@ -0,0 +1,18 @@ +[ + ; "function", + (bind) + (assert) + (with) + (let) + (if) + + (attrset) + (list) + (indented_string) + (parenthesized) +] @indent + +[ + "}" + "]" +] @outdent diff --git a/runtime/queries/nix/indents.toml b/runtime/queries/nix/indents.toml deleted file mode 100644 index 1d5c487187c6..000000000000 --- a/runtime/queries/nix/indents.toml +++ /dev/null @@ -1,18 +0,0 @@ -indent.tail = [ - # "function", - "bind", - "assert", - "with", - "let", - "if", - - "attrset", - "list", - "indented_string", - "parenthesized", -] - -outdent.all = [ - "}", - "]", -] diff --git a/runtime/queries/ocaml/indents.scm b/runtime/queries/ocaml/indents.scm new file mode 100644 index 000000000000..dc4d591a5456 --- /dev/null +++ b/runtime/queries/ocaml/indents.scm @@ -0,0 +1,12 @@ +[ + (let_binding) + (type_binding) + (structure) + (signature) + (record_declaration) + (function_expression) + (match_case) +] @indent + +"}" @outdent + diff --git a/runtime/queries/ocaml/indents.toml b/runtime/queries/ocaml/indents.toml deleted file mode 100644 index 660c34df0e9a..000000000000 --- a/runtime/queries/ocaml/indents.toml +++ /dev/null @@ -1,13 +0,0 @@ -indent.tail = [ - "let_binding", - "type_binding", - "structure", - "signature", - "record_declaration", - "function_expression", - "match_case", -] - -outdent.all = [ - "}", -] diff --git a/runtime/queries/perl/indents.scm b/runtime/queries/perl/indents.scm new file mode 100644 index 000000000000..5ae34f5e9842 --- /dev/null +++ b/runtime/queries/perl/indents.scm @@ -0,0 +1,15 @@ +[ + (function) + (identifier) + (method_invocation) + (if_statement) + (unless_statement) + (if_simple_statement) + (unless_simple_statement) + (variable_declaration) + (block) + (list_item) + (word_list_qw) +] @indent + +"}" @outdent diff --git a/runtime/queries/perl/indents.toml b/runtime/queries/perl/indents.toml deleted file mode 100644 index 6116b26b98c0..000000000000 --- a/runtime/queries/perl/indents.toml +++ /dev/null @@ -1,17 +0,0 @@ -indent.tail = [ - "function", - "identifier", - "method_invocation", - "if_statement", - "unless_statement", - "if_simple_statement", - "unless_simple_statement", - "variable_declaration", - "block", - "list_item", - "word_list_qw" -] - -outdent.all = [ - "}" -] diff --git a/runtime/queries/php/indents.scm b/runtime/queries/php/indents.scm new file mode 100644 index 000000000000..b22393ed18ef --- /dev/null +++ b/runtime/queries/php/indents.scm @@ -0,0 +1,17 @@ +[ + (array_creation_expression) + (arguments) + (formal_parameters) + (compound_statement) + (declaration_list) + (binary_expression) + (return_statement) + (expression_statement) + (switch_block) + (anonymous_function_use_clause) +] @indent + +[ + "}" + ")" +] @outdent diff --git a/runtime/queries/php/indents.toml b/runtime/queries/php/indents.toml deleted file mode 100644 index 2f30c6d1829c..000000000000 --- a/runtime/queries/php/indents.toml +++ /dev/null @@ -1,17 +0,0 @@ -indent.tail = [ - "array_creation_expression", - "arguments", - "formal_parameters", - "compound_statement", - "declaration_list", - "binary_expression", - "return_statement", - "expression_statement", - "switch_block", - "anonymous_function_use_clause", -] - -oudent = [ - "}", - ")", -] diff --git a/runtime/queries/protobuf/indents.scm b/runtime/queries/protobuf/indents.scm new file mode 100644 index 000000000000..d457d75f1998 --- /dev/null +++ b/runtime/queries/protobuf/indents.scm @@ -0,0 +1,11 @@ +[ + (messageBody) + (enumBody) + (oneofBody) + (serviceBody) + (rpcBody) + (msgLit) +] @indent + +"}" @outdent + diff --git a/runtime/queries/protobuf/indents.toml b/runtime/queries/protobuf/indents.toml deleted file mode 100644 index f0155d559524..000000000000 --- a/runtime/queries/protobuf/indents.toml +++ /dev/null @@ -1,12 +0,0 @@ -indent.tail = [ - "messageBody", - "enumBody", - "oneofBody", - "serviceBody", - "rpcBody", - "msgLit", -] - -outdent.all = [ - "}", -] diff --git a/runtime/queries/python/indents.scm b/runtime/queries/python/indents.scm new file mode 100644 index 000000000000..810ff52f5e8f --- /dev/null +++ b/runtime/queries/python/indents.scm @@ -0,0 +1,38 @@ +[ + (list) + (tuple) + (dictionary) + (set) + + (if_statement) + (for_statement) + (while_statement) + (with_statement) + (try_statement) + (import_from_statement) + + (parenthesized_expression) + (generator_expression) + (list_comprehension) + (set_comprehension) + (dictionary_comprehension) + + (tuple_pattern) + (list_pattern) + (argument_list) + (parameters) + (binary_operator) + + (function_definition) + (class_definition) +] @indent + +[ + ")" + "]" + "}" + (return_statement) + (pass_statement) + (raise_statement) +] @outdent + diff --git a/runtime/queries/python/indents.toml b/runtime/queries/python/indents.toml deleted file mode 100644 index a458c5715666..000000000000 --- a/runtime/queries/python/indents.toml +++ /dev/null @@ -1,39 +0,0 @@ -indent.tail = [ - "list", - "tuple", - "dictionary", - "set", - - "if_statement", - "for_statement", - "while_statement", - "with_statement", - "try_statement", - "import_from_statement", - - "parenthesized_expression", - "generator_expression", - "list_comprehension", - "set_comprehension", - "dictionary_comprehension", - - "tuple_pattern", - "list_pattern", - "argument_list", - "parameters", - "binary_operator", - - "function_definition", - "class_definition", -] - -outdent.all = [ - ")", - "]", - "}", - "return_statement", - "pass_statement", - "raise_statement", -] - -ignore = ["string"] diff --git a/runtime/queries/ruby/indents.scm b/runtime/queries/ruby/indents.scm new file mode 100644 index 000000000000..f5a6d19b5e03 --- /dev/null +++ b/runtime/queries/ruby/indents.scm @@ -0,0 +1,25 @@ +[ + (argument_list) + (array) + (begin) + (block) + (call) + (class) + (case) + (do_block) + (elsif) + (if) + (hash) + (method) + (module) + (singleton_class) + (singleton_method) +] @indent + +[ + ")" + "}" + "]" + "end" + "when" +] @outdent diff --git a/runtime/queries/ruby/indents.toml b/runtime/queries/ruby/indents.toml deleted file mode 100644 index 5549739028e1..000000000000 --- a/runtime/queries/ruby/indents.toml +++ /dev/null @@ -1,25 +0,0 @@ -indent.tail = [ - "argument_list", - "array", - "begin", - "block", - "call", - "class", - "case", - "do_block", - "elsif", - "if", - "hash", - "method", - "module", - "singleton_class", - "singleton_method", -] - -outdent.all = [ - ")", - "}", - "]", - "end", - "when", -] diff --git a/runtime/queries/rust/indents.scm b/runtime/queries/rust/indents.scm new file mode 100644 index 000000000000..2e2916e74dae --- /dev/null +++ b/runtime/queries/rust/indents.scm @@ -0,0 +1,38 @@ +[ + (use_list) + (block) + (match_block) + (arguments) + (parameters) + (declaration_list) + (field_declaration_list) + (field_initializer_list) + (struct_pattern) + (tuple_pattern) + (unit_expression) + (enum_variant_list) + (call_expression) + (binary_expression) + (field_expression) + (tuple_expression) + (array_expression) + (where_clause) + (macro_invocation) +] @indent + +[ + "where" + "}" + "]" + ")" +] @outdent + +(assignment_expression + right: (_) @indent + (#set! "scope" "all")) +(compound_assignment_expr + right: (_) @indent + (#set! "scope" "all")) +(let_declaration + value: (_) @indent + (#set! "scope" "all")) \ No newline at end of file diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml deleted file mode 100644 index 07c5b8f45560..000000000000 --- a/runtime/queries/rust/indents.toml +++ /dev/null @@ -1,34 +0,0 @@ -[indent] -all = [ - { parent_kind_in = ["assignment_expression", "compound_assignment_expr"], field_name_in = ["right"]}, - { parent_kind_in = ["let_declaration"], field_name_in = ["value"] } -] -tail = [ - "use_list", - "block", - "match_block", - "arguments", - "parameters", - "declaration_list", - "field_declaration_list", - "field_initializer_list", - "struct_pattern", - "tuple_pattern", - "unit_expression", - "enum_variant_list", - "call_expression", - "binary_expression", - "field_expression", - "tuple_expression", - "array_expression", - "where_clause", - "macro_invocation" -] - -[outdent] -all = [ - "where", - "}", - "]", - ")" -] diff --git a/runtime/queries/scala/indents.scm b/runtime/queries/scala/indents.scm new file mode 100644 index 000000000000..e3fec2c18cbd --- /dev/null +++ b/runtime/queries/scala/indents.scm @@ -0,0 +1,22 @@ +[ + (block) + (arguments) + (parameter) + (class_definition) + (trait_definition) + (object_definition) + (function_definition) + (val_definition) + (import_declaration) + (while_expression) + (do_while_expression) + (for_expression) + (try_expression) + (match_expression) +] @indent + +[ + "}" + "]" + ")" +] @outdent diff --git a/runtime/queries/scala/indents.toml b/runtime/queries/scala/indents.toml deleted file mode 100644 index a34f0f2992ff..000000000000 --- a/runtime/queries/scala/indents.toml +++ /dev/null @@ -1,23 +0,0 @@ - -indent.tail = [ - "block", - "arguments", - "parameter", - "class_definition", - "trait_definition", - "object_definition", - "function_definition", - "val_definition", - "import_declaration", - "while_expression", - "do_while_expression", - "for_expression", - "try_expression", - "match_expression" -] - -outdent.all = [ - "}", - "]", - ")" -] diff --git a/runtime/queries/svelte/indents.scm b/runtime/queries/svelte/indents.scm new file mode 100644 index 000000000000..02aaaa58de33 --- /dev/null +++ b/runtime/queries/svelte/indents.scm @@ -0,0 +1,17 @@ +[ + (element) + (if_statement) + (each_statement) + (await_statement) +] @indent + +[ + (end_tag) + (else_statement) + (if_end_expr) + (each_end_expr) + (await_end_expr) + ">" + "/>" +] @outdent + diff --git a/runtime/queries/svelte/indents.toml b/runtime/queries/svelte/indents.toml deleted file mode 100644 index b04247d7557e..000000000000 --- a/runtime/queries/svelte/indents.toml +++ /dev/null @@ -1,18 +0,0 @@ -indent.tail = [ - "element" - "if_statement" - "each_statement" - "await_statement" -] - -outdent.all = [ - "end_tag" - "else_statement" - "if_end_expr" - "each_end_expr" - "await_end_expr" - ">" - "/>" -] - -ignore = "comment" diff --git a/runtime/queries/tablegen/indents.scm b/runtime/queries/tablegen/indents.scm new file mode 100644 index 000000000000..1c15d7dbd32b --- /dev/null +++ b/runtime/queries/tablegen/indents.scm @@ -0,0 +1,3 @@ +(statement) @indent + +"}" @outdent diff --git a/runtime/queries/tablegen/indents.toml b/runtime/queries/tablegen/indents.toml deleted file mode 100644 index abea12f2e54c..000000000000 --- a/runtime/queries/tablegen/indents.toml +++ /dev/null @@ -1,7 +0,0 @@ -indent.tail = [ - "statement", -] - -outdent.all = [ - "}", -] diff --git a/runtime/queries/typescript/indents.scm b/runtime/queries/typescript/indents.scm new file mode 100644 index 000000000000..055e170b82f2 --- /dev/null +++ b/runtime/queries/typescript/indents.scm @@ -0,0 +1,7 @@ +; inherits: javascript + +[ + (enum_declaration) + (interface_declaration) + (object_type) +] @indent diff --git a/runtime/queries/typescript/indents.toml b/runtime/queries/typescript/indents.toml deleted file mode 120000 index 3a17f2586608..000000000000 --- a/runtime/queries/typescript/indents.toml +++ /dev/null @@ -1 +0,0 @@ -../javascript/indents.toml \ No newline at end of file diff --git a/runtime/queries/yaml/indents.scm b/runtime/queries/yaml/indents.scm new file mode 100644 index 000000000000..70a00b695389 --- /dev/null +++ b/runtime/queries/yaml/indents.scm @@ -0,0 +1,2 @@ +(block_mapping_pair) @indent + diff --git a/runtime/queries/yaml/indents.toml b/runtime/queries/yaml/indents.toml deleted file mode 100644 index 499994f7a190..000000000000 --- a/runtime/queries/yaml/indents.toml +++ /dev/null @@ -1,3 +0,0 @@ -indent.tail = [ - "block_mapping_pair", -] diff --git a/runtime/queries/zig/indents.scm b/runtime/queries/zig/indents.scm new file mode 100644 index 000000000000..ad31b01f38ce --- /dev/null +++ b/runtime/queries/zig/indents.scm @@ -0,0 +1,16 @@ +[ + (Block) + (BlockExpr) + (ContainerDecl) + (SwitchExpr) + (AssignExpr) + (ErrorUnionExpr) + (Statement) + (InitList) +] @indent + +[ + "}" + "]" + ")" +] @outdent diff --git a/runtime/queries/zig/indents.toml b/runtime/queries/zig/indents.toml deleted file mode 100644 index 234992dec957..000000000000 --- a/runtime/queries/zig/indents.toml +++ /dev/null @@ -1,16 +0,0 @@ -indent.tail = [ - "Block", - "BlockExpr", - "ContainerDecl", - "SwitchExpr", - "AssignExpr", - "ErrorUnionExpr", - "Statement", - "InitList" -] - -outdent.all = [ - "}", - "]", - ")" -] From c6f6a2d6a483618c3feb20b2333be6c3d7299eb2 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sat, 12 Feb 2022 23:50:34 +0100 Subject: [PATCH 09/19] Add documentation for the new indent queries. Change xtask docgen to look for indents.scm instead of indents.toml --- book/src/SUMMARY.md | 1 + book/src/guides/indent.md | 47 +++++++++++++++++++++++++++++++++++++++ xtask/src/main.rs | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 book/src/guides/indent.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index a8f165c01797..80edca01fdfe 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -14,3 +14,4 @@ - [Guides](./guides/README.md) - [Adding Languages](./guides/adding_languages.md) - [Adding Textobject Queries](./guides/textobject.md) + - [Adding Indent Queries](./guides/indent.md) diff --git a/book/src/guides/indent.md b/book/src/guides/indent.md new file mode 100644 index 000000000000..a25fb331cac7 --- /dev/null +++ b/book/src/guides/indent.md @@ -0,0 +1,47 @@ +# Adding Indent Queries + +Helix uses tree-sitter to correctly indent new lines. This requires +a tree-sitter grammar and an `indent.scm` query file placed in +`runtime/queries/{language}/indents.scm`. The indentation for a line +is calculated by traversing the syntax tree from the lowest node at the +beginning of the new line. Each of these nodes contributes to the total +indent when it is captured by the query (in what way depends on the name +of the capture). + +Note that it matters where these added indents begin. For example, +multiple indent level increases that start on the same line only increase +the total indent level by 1. + +## Scopes + +Added indents don't always apply to the whole node. For example, in most +cases when a node should be indented, we actually only want everything +except for its first line to be indented. For this, there are several +scopes (more scopes may be added in the future if required): + +- `all`: +This scope applies to the whole captured node. This is only different from +`tail` when the captured node is the first node on its line. + +- `tail`: +This scope applies to everything except for the first line of the +captured node. + +Every capture type has a default scope which should do the right thing +in most situations. When a different scope is required, this can be +changed by using a `#set!` declaration anywhere in the pattern: +```scm +(assignment_expression + right: (_) @indent + (#set! "scope" "all")) +``` + +## Capture Types + +- `@indent` (default scope `tail`): +Increase the indent level by 1. Multiple occurences in the same line +don't stack. If there is at least one `@indent` and one `@outdent` +capture on the same line, the indent level isn't changed at all. + +- `@outdent` (default scope `all`): +Decrease the indent level by 1. The same rules as for `@indent` apply. diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 7256653a38c1..b4b858e95f9f 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -27,7 +27,7 @@ pub mod helpers { match *self { Self::Highlight => "highlights.scm", Self::TextObjects => "textobjects.scm", - Self::AutoIndent => "indents.toml", + Self::AutoIndent => "indents.scm", } } } From 5d8e1d863d0d925fd12917a4e2fbcf63be84b3e9 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Tue, 15 Feb 2022 20:45:16 +0100 Subject: [PATCH 10/19] Improve code style in indent.rs. Fix an issue with the rust indent query. --- helix-core/src/indent.rs | 110 +++++++++++++++---------------- runtime/queries/rust/indents.scm | 12 +++- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index be9a27ead391..0f6fa69e7c47 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,4 +1,4 @@ -use std::collections::{hash_map::Entry, HashMap}; +use std::collections::HashMap; use tree_sitter::{Query, QueryCursor}; @@ -226,56 +226,52 @@ fn get_first_in_line(mut node: Node, byte_pos: usize, new_line: bool) -> Vec Self { - IndentResult { indent: 0 } - } - /// Add the given [AddedIndent] to the [IndentResult]. - /// The [AddedIndent] should be the combination of all the added indents for one line. - fn add(&mut self, added: &AddedIndent) { - if added.indent && !added.outdent { - self.indent += 1; - } else if added.outdent && !added.indent { - self.indent -= 1; + IndentResult { + indent: 0, + outdent: 0, } } - fn as_string(&self, indent_style: &IndentStyle) -> String { - indent_style.as_str().repeat(0.max(self.indent) as usize) - } -} - -/// The indent that is added for a single line. -/// This is different from the total indent ([IndentResult]) because multiple indents/outdents on the same line don't stack. -/// It should be constructed by successively adding the relevant indent captures. -struct AddedIndent { - indent: bool, - outdent: bool, -} -impl AddedIndent { - /// Returns the [AddedIndent] for a line without any indent definitions - fn new() -> Self { - AddedIndent { - indent: false, - outdent: false, + /// Add some other [IndentResult] to this. + /// The added indent should be the total added indent from one line + fn add_line(&mut self, added: &IndentResult) { + if added.indent > 0 && added.outdent == 0 { + self.indent += 1; + } else if added.outdent > 0 && added.indent == 0 { + self.outdent += 1; } } - /// Adds an indent capture to this indent. - fn add_capture(&mut self, capture: &IndentCaptureType) { - match capture { + /// Add an indent capture to this indent. + /// All the captures that are added in this way should be on the same line. + fn add_capture(&mut self, added: IndentCaptureType) { + match added { IndentCaptureType::Indent => { - self.indent = true; + self.indent = 1; } IndentCaptureType::Outdent => { - self.outdent = true; + self.outdent = 1; } } } + fn as_string(&self, indent_style: &IndentStyle) -> String { + let indent_level = if self.indent >= self.outdent { + self.indent - self.outdent + } else { + log::warn!("Encountered more outdent than indent nodes while calculating indentation: {} outdent, {} indent", self.outdent, self.indent); + 0 + }; + indent_style.as_str().repeat(indent_level) + } } /// An indent definition which corresponds to a capture from the indent query @@ -283,13 +279,14 @@ struct IndentCapture { capture_type: IndentCaptureType, scope: IndentScope, } +#[derive(Clone, Copy)] enum IndentCaptureType { Indent, Outdent, } impl IndentCaptureType { fn default_scope(&self) -> IndentScope { - match *self { + match self { IndentCaptureType::Indent => IndentScope::Tail, IndentCaptureType::Outdent => IndentScope::All, } @@ -298,6 +295,7 @@ impl IndentCaptureType { /// This defines which part of a node an [IndentCapture] applies to. /// Each [IndentCaptureType] has a default scope, but the scope can be changed /// with `#set!` property declarations. +#[derive(Clone, Copy)] enum IndentScope { /// The indent applies to the whole node All, @@ -338,7 +336,7 @@ fn query_indents( for property in query.property_settings(m.pattern_index) { match property.key.as_ref() { "scope" => { - indent_capture.scope = match property.value.as_ref().map(|s| s.as_ref()) { + indent_capture.scope = match property.value.as_deref() { Some("all") => IndentScope::All, Some("tail") => IndentScope::Tail, Some(s) => { @@ -359,14 +357,10 @@ fn query_indents( } } } - match indent_captures.entry(capture.node.id()) { - Entry::Occupied(mut e) => { - e.get_mut().push(indent_capture); - } - Entry::Vacant(e) => { - e.insert(vec![indent_capture]); - } - } + indent_captures + .entry(capture.node.id()) + .or_default() + .push(indent_capture); } } indent_captures @@ -428,8 +422,8 @@ fn treesitter_indent_for_pos( let mut result = IndentResult::new(); // We always keep track of all the indent changes on one line, in order to only indent once // even if there are multiple "indent" nodes on the same line - let mut indent_for_line = AddedIndent::new(); - let mut indent_for_line_below = AddedIndent::new(); + let mut indent_for_line = IndentResult::new(); + let mut indent_for_line_below = IndentResult::new(); loop { let is_first = *first_in_line.last().unwrap(); // Apply all indent definitions for this node @@ -438,13 +432,13 @@ fn treesitter_indent_for_pos( match definition.scope { IndentScope::All => { if is_first { - indent_for_line.add_capture(&definition.capture_type); + indent_for_line.add_capture(definition.capture_type); } else { - indent_for_line_below.add_capture(&definition.capture_type); + indent_for_line_below.add_capture(definition.capture_type); } } IndentScope::Tail => { - indent_for_line_below.add_capture(&definition.capture_type); + indent_for_line_below.add_capture(definition.capture_type); } } } @@ -453,7 +447,7 @@ fn treesitter_indent_for_pos( if let Some(parent) = node.parent() { let mut node_line = node.start_position().row; let mut parent_line = parent.start_position().row; - if node.start_position().row == line && new_line { + if node_line == line && new_line { // Also consider the line that will be inserted if node.start_byte() >= byte_pos { node_line += 1; @@ -465,22 +459,22 @@ fn treesitter_indent_for_pos( if node_line != parent_line { if node_line < line + (new_line as usize) { // Don't add indent for the line below the line of the query - result.add(&indent_for_line_below); + result.add_line(&indent_for_line_below); } if node_line == parent_line + 1 { indent_for_line_below = indent_for_line; } else { - result.add(&indent_for_line); - indent_for_line_below = AddedIndent::new(); + result.add_line(&indent_for_line); + indent_for_line_below = IndentResult::new(); } - indent_for_line = AddedIndent::new(); + indent_for_line = IndentResult::new(); } node = parent; first_in_line.pop(); } else { - result.add(&indent_for_line_below); - result.add(&indent_for_line); + result.add_line(&indent_for_line_below); + result.add_line(&indent_for_line); break; } } diff --git a/runtime/queries/rust/indents.scm b/runtime/queries/rust/indents.scm index 2e2916e74dae..863319f921d2 100644 --- a/runtime/queries/rust/indents.scm +++ b/runtime/queries/rust/indents.scm @@ -27,12 +27,18 @@ ")" ] @outdent +; TODO Add some mechanism to correctly align if-else statements here. +; For now they have to be excluded here because in some cases the else block +; is indented differently than the if block (assignment_expression right: (_) @indent - (#set! "scope" "all")) + (#set! "scope" "all") + (#not-match? @indent "if\\s")) (compound_assignment_expr right: (_) @indent - (#set! "scope" "all")) + (#set! "scope" "all") + (#not-match? @indent "if\\s")) (let_declaration value: (_) @indent - (#set! "scope" "all")) \ No newline at end of file + (#set! "scope" "all") + (#not-match? @indent "if\\s")) \ No newline at end of file From 3c066b546a095d61e573b769e734f4ac152396de Mon Sep 17 00:00:00 2001 From: Triton171 Date: Thu, 17 Feb 2022 13:10:59 +0100 Subject: [PATCH 11/19] Move indentation test sources to separate files. Add `#not-kind-eq?`, `#same-line?` and `#not-same-line` custom predicates. Improve the rust and c indent queries. --- book/src/guides/indent.md | 32 +++++ helix-core/src/indent.rs | 208 ++++++++++++----------------- helix-core/test-indent/commands.rs | 1 + helix-core/test-indent/rust.rs | 105 +++++++++++++++ runtime/queries/c/indents.scm | 10 +- runtime/queries/rust/indents.scm | 31 ++++- 6 files changed, 256 insertions(+), 131 deletions(-) create mode 120000 helix-core/test-indent/commands.rs create mode 100644 helix-core/test-indent/rust.rs diff --git a/book/src/guides/indent.md b/book/src/guides/indent.md index a25fb331cac7..235a30c442ea 100644 --- a/book/src/guides/indent.md +++ b/book/src/guides/indent.md @@ -45,3 +45,35 @@ capture on the same line, the indent level isn't changed at all. - `@outdent` (default scope `all`): Decrease the indent level by 1. The same rules as for `@indent` apply. + +## Predicates + +In some cases, an S-expression cannot express exactly what pattern should be matched. +For that, tree-sitter allows for predicates to appear anywhere within a pattern, +similar to how `#set!` declarations work: +```scm +(some_kind + (child_kind) @indent + (#predicate? arg1 arg2 ...) +) +``` +The number of arguments depends on the predicate that's used. +Each argument is either a capture (`@name`) or a string (`"some string"`). +The following predicates are supported by tree-sitter: + +- `#eq?`/`#not-eq?`: +The first argument (a capture) must/must not be equal to the second argument +(a capture or a string). + +- `#match?`/`#not-match?`: +The first argument (a capture) must/must not match the regex given in the +second argument (a string). + +Additionally, we support some custom predicates for indent queries: + +- `#not-kind-eq?`: +The kind of the first argument (a capture) must not be equal to the second +argument (a string). + +- `#same-line?`/`#not-same-line?`: +The captures given by the 2 arguments must/must not start on the same line. diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 0f6fa69e7c47..0124dc1dda6e 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use tree_sitter::{Query, QueryCursor}; +use tree_sitter::{Query, QueryCursor, QueryPredicateArg}; use crate::{ chars::{char_is_line_ending, char_is_whitespace}, @@ -309,14 +309,76 @@ fn query_indents( query: &Query, syntax: &Syntax, text: RopeSlice, - byte_pos: usize, + range: std::ops::Range, + // Position of the (optional) newly inserted line break. + // Given as (line, byte_pos) + new_line_break: Option<(usize, usize)>, ) -> HashMap> { // TODO Maybe reuse this cursor? let mut cursor = QueryCursor::new(); let mut indent_captures: HashMap> = HashMap::new(); - cursor.set_byte_range(byte_pos..byte_pos + 1); + cursor.set_byte_range(range); // Iterate over all captures from the query for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) { + // Skip matches where not all custom predicates are fulfilled + 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 get_line_num = |node: Node| { + let mut node_line = node.start_position().row; + // Adjust for the new line that will be inserted + if let Some((line, byte)) = new_line_break { + if node_line==line && node.start_byte()>=byte { + node_line += 1; + } + } + node_line + }; + 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 same_line = get_line_num(n1)==get_line_num(n2); + same_line==(pred.operator.as_ref()=="same-line?") + } + _ => true, + } + } + _ => { + panic!("Invalid indent query: Arguments to \"{}\" must be 2 captures", pred.operator); + } + } + } + _ => { + panic!( + "Invalid indent query: Unknown predicate (\"{}\")", + pred.operator + ); + } + } + }) { + continue; + } for capture in m.captures { let capture_type = query.capture_names()[capture.index as usize].as_str(); let capture_type = match capture_type { @@ -417,7 +479,12 @@ fn treesitter_indent_for_pos( .root_node() .descendant_for_byte_range(byte_pos, byte_pos)?; let mut first_in_line = get_first_in_line(node, byte_pos, new_line); - let query_result = query_indents(query, syntax, text, byte_pos); + let new_line_break = if new_line { + Some((line, byte_pos)) + } else { + None + }; + let query_result = query_indents(query, syntax, text, byte_pos..byte_pos + 1, new_line_break); let mut result = IndentResult::new(); // We always keep track of all the indent changes on one line, in order to only indent once @@ -557,123 +624,25 @@ mod test { } #[test] - fn test_suggested_indent_for_line() { - let doc = Rope::from( - " -use std::{ - io::{self, stdout, Stdout, Write}, - path::PathBuf, - sync::Arc, - time::Duration, -} -mod test { - fn hello_world() { - 1 + 1; - - let does_indentation_work = 1; - - let mut really_long_variable_name_using_up_the_line = - really_long_fn_that_should_definitely_go_on_the_next_line(); - really_long_variable_name_using_up_the_line = - really_long_fn_that_should_definitely_go_on_the_next_line(); - really_long_variable_name_using_up_the_line |= - really_long_fn_that_should_definitely_go_on_the_next_line(); - - let ( - a_long_variable_name_in_this_tuple, - b_long_variable_name_in_this_tuple, - c_long_variable_name_in_this_tuple, - d_long_variable_name_in_this_tuple, - e_long_variable_name_in_this_tuple, - ): (usize, usize, usize, usize, usize) = - if really_long_fn_that_should_definitely_go_on_the_next_line() { - ( - 03294239434, - 1213412342314, - 21231234134, - 834534234549898789, - 9879234234543853457, - ) - } else { - (0, 1, 2, 3, 4) - }; - - let test_function = function_with_param(this_param, - that_param - ); - - let test_function = function_with_param( - this_param, - that_param - ); - - let test_function = function_with_proper_indent(param1, - param2, - ); - - let selection = Selection::new( - changes - .clone() - .map(|(start, end, text): (usize, usize, Option)| { - let len = text.map(|text| text.len()).unwrap() - 1; // minus newline - let pos = start + len; - Range::new(pos, pos) - }) - .collect(), - 0, - ); - - return; + fn test_treesitter_indent_rust() { + test_treesitter_indent("rust.rs", "source.rust"); } -} - -impl MyTrait for YourType -where - A: TraitB + TraitC, - D: TraitE + TraitF, -{ - -} -#[test] -// -match test { - Some(a) => 1, - None => { - unimplemented!() + #[test] + fn test_treesitter_indent_rust_2() { + test_treesitter_indent("commands.rs", "source.rust"); } -} -std::panic::set_hook(Box::new(move |info| { - hook(info); -})); - -{ { { - 1 -}}} - -pub fn change(document: &Document, changes: I) -> Self -where - I: IntoIterator + ExactSizeIterator, -{ - [ - 1, - 2, - 3, - ]; - ( - 1, - 2 - ); - true -} -", - ); - let doc = doc; + fn test_treesitter_indent(file_name: &str, lang_scope: &str) { use crate::diagnostic::Severity; - use crate::syntax::{ - Configuration, IndentationConfiguration, LanguageConfiguration, Loader, - }; + use crate::syntax::{Configuration, IndentationConfiguration, Loader}; use once_cell::sync::OnceCell; + use std::path::PathBuf; + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test-indent"); + path.push(file_name); + let file = std::fs::File::open(path).unwrap(); + let doc = ropey::Rope::from_reader(file).unwrap(); + let loader = Loader::new(Configuration { language: vec![LanguageConfiguration { scope: "source.rust".to_string(), @@ -704,16 +673,17 @@ where runtime.push("../runtime"); std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap()); - let language_config = loader.language_config_for_scope("source.rust").unwrap(); + let language_config = loader.language_config_for_scope(lang_scope).unwrap(); let highlight_config = language_config.highlight_config(&[]).unwrap(); let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)); + let indent_query = language_config.indent_query().unwrap(); let text = doc.slice(..); for i in 0..doc.len_lines() { let line = text.line(i); if let Some(pos) = crate::find_first_non_whitespace_char(line) { let suggested_indent = treesitter_indent_for_pos( - language_config.indent_query().unwrap(), + indent_query, &syntax, &IndentStyle::Spaces(4), text, @@ -726,7 +696,7 @@ where line.get_slice(..suggested_indent.chars().count()) .map_or(false, |s| s == suggested_indent), "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n", - i, + i+1, line.slice(..line.len_chars()-1), suggested_indent, ); diff --git a/helix-core/test-indent/commands.rs b/helix-core/test-indent/commands.rs new file mode 120000 index 000000000000..d28fd5eead97 --- /dev/null +++ b/helix-core/test-indent/commands.rs @@ -0,0 +1 @@ +../../helix-term/src/commands.rs \ No newline at end of file diff --git a/helix-core/test-indent/rust.rs b/helix-core/test-indent/rust.rs new file mode 100644 index 000000000000..010745e0d708 --- /dev/null +++ b/helix-core/test-indent/rust.rs @@ -0,0 +1,105 @@ +use std::{ + io::{self, stdout, Stdout, Write}, + path::PathBuf, + sync::Arc, + time::Duration, +}; +mod test { + fn hello_world() { + 1 + 1; + + let does_indentation_work = 1; + + let mut really_long_variable_name_using_up_the_line = + really_long_fn_that_should_definitely_go_on_the_next_line(); + really_long_variable_name_using_up_the_line = + really_long_fn_that_should_definitely_go_on_the_next_line(); + really_long_variable_name_using_up_the_line |= + really_long_fn_that_should_definitely_go_on_the_next_line(); + + let ( + a_long_variable_name_in_this_tuple, + b_long_variable_name_in_this_tuple, + c_long_variable_name_in_this_tuple, + d_long_variable_name_in_this_tuple, + e_long_variable_name_in_this_tuple, + ): (usize, usize, usize, usize, usize) = + if really_long_fn_that_should_definitely_go_on_the_next_line() { + ( + 03294239434, + 1213412342314, + 21231234134, + 834534234549898789, + 9879234234543853457, + ) + } else { + (0, 1, 2, 3, 4) + }; + + let test_function = function_with_param(this_param, + that_param + ); + + let test_function = function_with_param( + this_param, + that_param + ); + + let test_function = function_with_proper_indent(param1, + param2, + ); + + let selection = Selection::new( + changes + .clone() + .map(|(start, end, text): (usize, usize, Option)| { + let len = text.map(|text| text.len()).unwrap() - 1; // minus newline + let pos = start + len; + Range::new(pos, pos) + }) + .collect(), + 0, + ); + + return; + } +} + +impl MyTrait for YourType +where + A: TraitB + TraitC, + D: TraitE + TraitF, +{ + +} +#[test] +// +match test { + Some(a) => 1, + None => { + unimplemented!() + } +} +std::panic::set_hook(Box::new(move |info| { + hook(info); +})); + +{ { { + 1 +}}} + +pub fn change(document: &Document, changes: I) -> Self +where + I: IntoIterator + ExactSizeIterator, +{ + [ + 1, + 2, + 3, + ]; + ( + 1, + 2 + ); + true +} diff --git a/runtime/queries/c/indents.scm b/runtime/queries/c/indents.scm index b080daf50743..353ea81b9a2e 100644 --- a/runtime/queries/c/indents.scm +++ b/runtime/queries/c/indents.scm @@ -14,22 +14,20 @@ "]" ] @outdent -; The #not-match? is just required to exclude compound statements. -; It would be nice to do this somehow without regexes (if_statement consequence: (_) @indent - (#not-match? @indent "\\\{*\\\}") + (#not-kind-eq? @indent "compound_statement") (#set! "scope" "all")) (while_statement body: (_) @indent - (#not-match? @indent "\\\{*\\\}") + (#not-kind-eq? @indent "compound_statement") (#set! "scope" "all")) (do_statement body: (_) @indent - (#not-match? @indent "\\\{*\\\}") + (#not-kind-eq? @indent "compound_statement") (#set! "scope" "all")) (for_statement ")" (_) @indent - (#not-match? @indent "\\\{*\\\}") + (#not-kind-eq? @indent "compound_statement") (#set! "scope" "all")) diff --git a/runtime/queries/rust/indents.scm b/runtime/queries/rust/indents.scm index 863319f921d2..1ffefd3468d6 100644 --- a/runtime/queries/rust/indents.scm +++ b/runtime/queries/rust/indents.scm @@ -27,18 +27,37 @@ ")" ] @outdent -; TODO Add some mechanism to correctly align if-else statements here. -; For now they have to be excluded here because in some cases the else block -; is indented differently than the if block +; Indent the right side of assignments. +; The #not-same-line? predicate is required to prevent an extra indent for e.g. +; an else-clause where the previous if-clause starts on the same line as the assignment. (assignment_expression + . + (_) @expr-start right: (_) @indent + (#not-same-line? @indent @expr-start) (#set! "scope" "all") - (#not-match? @indent "if\\s")) +) (compound_assignment_expr + . + (_) @expr-start right: (_) @indent + (#not-same-line? @indent @expr-start) (#set! "scope" "all") - (#not-match? @indent "if\\s")) +) (let_declaration + . + (_) @expr-start value: (_) @indent + (#not-same-line? @indent @expr-start) (#set! "scope" "all") - (#not-match? @indent "if\\s")) \ No newline at end of file +) + +; Some field expressions where the left part is a multiline expression are not +; indented by cargo fmt. +; Because this multiline expression might be nested in an arbitrary number of +; field expressions, this can only be matched using a Regex. +(field_expression + value: (_) @val + "." @outdent + (#match? @val "([^\\n]+\\([\\t ]*\\n.*)|([^\\n]*\\{[\\t ]*\\n)") +) From 7c64788e837880d97d3eb69bcadd200e17c63a91 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Thu, 17 Feb 2022 19:23:42 +0100 Subject: [PATCH 12/19] Fix indent test. Improve rust indent queries. --- helix-core/src/indent.rs | 8 +++++--- helix-core/test-indent/indent.rs | 1 + runtime/queries/rust/indents.scm | 23 ++++++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) create mode 120000 helix-core/test-indent/indent.rs diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index ce3b5f7e05dc..2eb5fa8b051f 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -629,7 +629,10 @@ mod test { } #[test] fn test_treesitter_indent_rust_2() { - test_treesitter_indent("commands.rs", "source.rust"); + test_treesitter_indent("indent.rs", "source.rust"); + // TODO Use commands.rs as indentation test. + // Currently this fails because we can't align the parameters of a closure yet + // test_treesitter_indent("commands.rs", "source.rust"); } fn test_treesitter_indent(file_name: &str, lang_scope: &str) { @@ -694,8 +697,7 @@ mod test { ) .unwrap(); assert!( - line.get_slice(..suggested_indent.chars().count()) - .map_or(false, |s| s == suggested_indent), + line.get_slice(..pos).map_or(false, |s| s == suggested_indent), "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n", i+1, line.slice(..line.len_chars()-1), diff --git a/helix-core/test-indent/indent.rs b/helix-core/test-indent/indent.rs new file mode 120000 index 000000000000..812d714fff2e --- /dev/null +++ b/helix-core/test-indent/indent.rs @@ -0,0 +1 @@ +../src/indent.rs \ No newline at end of file diff --git a/runtime/queries/rust/indents.scm b/runtime/queries/rust/indents.scm index 1ffefd3468d6..600c77a32cd3 100644 --- a/runtime/queries/rust/indents.scm +++ b/runtime/queries/rust/indents.scm @@ -17,11 +17,14 @@ (tuple_expression) (array_expression) (where_clause) - (macro_invocation) + + (token_tree) + (macro_definition) + (token_tree_pattern) + (token_repetition) ] @indent [ - "where" "}" "]" ")" @@ -51,6 +54,20 @@ (#not-same-line? @indent @expr-start) (#set! "scope" "all") ) +(if_let_expression + . + (_) @expr-start + value: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(static_item + . + (_) @expr-start + value: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) ; Some field expressions where the left part is a multiline expression are not ; indented by cargo fmt. @@ -59,5 +76,5 @@ (field_expression value: (_) @val "." @outdent - (#match? @val "([^\\n]+\\([\\t ]*\\n.*)|([^\\n]*\\{[\\t ]*\\n)") + (#match? @val "(\\A[^\\n\\r]+\\([\\t ]*(\\n|\\r).*)|(\\A[^\\n\\r]*\\{[\\t ]*(\\n|\\r))") ) From 09d7d5d444e13fecacb706ab74c6be015a1fa223 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Fri, 25 Feb 2022 16:25:43 +0100 Subject: [PATCH 13/19] Move indentation tests to integration test folder. --- helix-core/src/indent.rs | 86 +----------------- helix-core/src/syntax.rs | 6 +- helix-core/test-indent/commands.rs | 1 - helix-core/test-indent/indent.rs | 1 - helix-core/tests/data/indent/indent.rs | 1 + .../data/indent}/rust.rs | 0 helix-core/tests/indent.rs | 88 +++++++++++++++++++ 7 files changed, 93 insertions(+), 90 deletions(-) delete mode 120000 helix-core/test-indent/commands.rs delete mode 120000 helix-core/test-indent/indent.rs create mode 120000 helix-core/tests/data/indent/indent.rs rename helix-core/{test-indent => tests/data/indent}/rust.rs (100%) create mode 100644 helix-core/tests/indent.rs diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 2eb5fa8b051f..f6efd5107edd 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -464,7 +464,7 @@ fn query_indents( /// }, /// ); /// ``` -fn treesitter_indent_for_pos( +pub fn treesitter_indent_for_pos( query: &Query, syntax: &Syntax, indent_style: &IndentStyle, @@ -622,88 +622,4 @@ mod test { let line = Rope::from("\t \tfn new"); // 1 tab, 4 spaces, tab assert_eq!(indent_level_for_line(line.slice(..), tab_width), 3); } - - #[test] - fn test_treesitter_indent_rust() { - test_treesitter_indent("rust.rs", "source.rust"); - } - #[test] - fn test_treesitter_indent_rust_2() { - test_treesitter_indent("indent.rs", "source.rust"); - // TODO Use commands.rs as indentation test. - // Currently this fails because we can't align the parameters of a closure yet - // test_treesitter_indent("commands.rs", "source.rust"); - } - - fn test_treesitter_indent(file_name: &str, lang_scope: &str) { - use crate::diagnostic::Severity; - use crate::syntax::{Configuration, IndentationConfiguration, Loader}; - use once_cell::sync::OnceCell; - use std::path::PathBuf; - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test-indent"); - path.push(file_name); - let file = std::fs::File::open(path).unwrap(); - let doc = ropey::Rope::from_reader(file).unwrap(); - - let loader = Loader::new(Configuration { - language: vec![LanguageConfiguration { - scope: "source.rust".to_string(), - file_types: vec!["rs".to_string()], - shebangs: vec![], - language_id: "Rust".to_string(), - highlight_config: OnceCell::new(), - config: None, - // - injection_regex: None, - roots: vec![], - comment_token: None, - auto_format: false, - diagnostic_severity: Severity::Warning, - tree_sitter_library: None, - language_server: None, - indent: Some(IndentationConfiguration { - tab_width: 4, - unit: String::from(" "), - }), - indent_query: OnceCell::new(), - textobject_query: OnceCell::new(), - debugger: None, - }], - }); - - // set runtime path so we can find the queries - let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - runtime.push("../runtime"); - std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap()); - - let language_config = loader.language_config_for_scope(lang_scope).unwrap(); - let highlight_config = language_config.highlight_config(&[]).unwrap(); - let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)); - let indent_query = language_config.indent_query().unwrap(); - let text = doc.slice(..); - - for i in 0..doc.len_lines() { - let line = text.line(i); - if let Some(pos) = crate::find_first_non_whitespace_char(line) { - let suggested_indent = treesitter_indent_for_pos( - indent_query, - &syntax, - &IndentStyle::Spaces(4), - text, - i, - text.line_to_char(i) + pos, - false, - ) - .unwrap(); - assert!( - line.get_slice(..pos).map_or(false, |s| s == suggested_indent), - "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n", - i+1, - line.slice(..line.len_chars()-1), - suggested_indent, - ); - } - } - } } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 8b5a323d2901..98dcdfa61234 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -76,7 +76,7 @@ pub struct LanguageConfiguration { // first_line_regex // #[serde(skip)] - pub(crate) highlight_config: OnceCell>>, + pub highlight_config: OnceCell>>, // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 #[serde(skip_serializing_if = "Option::is_none")] pub language_server: Option, @@ -84,9 +84,9 @@ pub struct LanguageConfiguration { pub indent: Option, #[serde(skip)] - pub(crate) indent_query: OnceCell>, + pub indent_query: OnceCell>, #[serde(skip)] - pub(crate) textobject_query: OnceCell>, + pub textobject_query: OnceCell>, #[serde(skip_serializing_if = "Option::is_none")] pub debugger: Option, } diff --git a/helix-core/test-indent/commands.rs b/helix-core/test-indent/commands.rs deleted file mode 120000 index d28fd5eead97..000000000000 --- a/helix-core/test-indent/commands.rs +++ /dev/null @@ -1 +0,0 @@ -../../helix-term/src/commands.rs \ No newline at end of file diff --git a/helix-core/test-indent/indent.rs b/helix-core/test-indent/indent.rs deleted file mode 120000 index 812d714fff2e..000000000000 --- a/helix-core/test-indent/indent.rs +++ /dev/null @@ -1 +0,0 @@ -../src/indent.rs \ No newline at end of file diff --git a/helix-core/tests/data/indent/indent.rs b/helix-core/tests/data/indent/indent.rs new file mode 120000 index 000000000000..2ac16cf96357 --- /dev/null +++ b/helix-core/tests/data/indent/indent.rs @@ -0,0 +1 @@ +../../../src/indent.rs \ No newline at end of file diff --git a/helix-core/test-indent/rust.rs b/helix-core/tests/data/indent/rust.rs similarity index 100% rename from helix-core/test-indent/rust.rs rename to helix-core/tests/data/indent/rust.rs diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs new file mode 100644 index 000000000000..32c3de2eb093 --- /dev/null +++ b/helix-core/tests/indent.rs @@ -0,0 +1,88 @@ +use helix_core::{ + diagnostic::Severity, + indent::{treesitter_indent_for_pos, IndentStyle}, + syntax::{Configuration, IndentationConfiguration, LanguageConfiguration, Loader}, + Syntax, +}; +use once_cell::sync::OnceCell; +use std::path::PathBuf; + +#[test] +fn test_treesitter_indent_rust() { + test_treesitter_indent("rust.rs", "source.rust"); +} +#[test] +fn test_treesitter_indent_rust_2() { + test_treesitter_indent("indent.rs", "source.rust"); + // TODO Use commands.rs as indentation test. + // Currently this fails because we can't align the parameters of a closure yet + // test_treesitter_indent("commands.rs", "source.rust"); +} + +fn test_treesitter_indent(file_name: &str, lang_scope: &str) { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data/indent"); + path.push(file_name); + let file = std::fs::File::open(path).unwrap(); + let doc = ropey::Rope::from_reader(file).unwrap(); + + let loader = Loader::new(Configuration { + language: vec![LanguageConfiguration { + scope: "source.rust".to_string(), + file_types: vec!["rs".to_string()], + shebangs: vec![], + language_id: "Rust".to_string(), + highlight_config: OnceCell::new(), + config: None, + // + injection_regex: None, + roots: vec![], + comment_token: None, + auto_format: false, + diagnostic_severity: Severity::Warning, + tree_sitter_library: None, + language_server: None, + indent: Some(IndentationConfiguration { + tab_width: 4, + unit: String::from(" "), + }), + indent_query: OnceCell::new(), + textobject_query: OnceCell::new(), + debugger: None, + }], + }); + + // set runtime path so we can find the queries + let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + runtime.push("../runtime"); + std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap()); + + let language_config = loader.language_config_for_scope(lang_scope).unwrap(); + let highlight_config = language_config.highlight_config(&[]).unwrap(); + let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)); + let indent_query = language_config.indent_query().unwrap(); + let text = doc.slice(..); + + for i in 0..doc.len_lines() { + let line = text.line(i); + if let Some(pos) = helix_core::find_first_non_whitespace_char(line) { + let suggested_indent = treesitter_indent_for_pos( + indent_query, + &syntax, + &IndentStyle::Spaces(4), + text, + i, + text.line_to_char(i) + pos, + false, + ) + .unwrap(); + assert!( + line.get_slice(..pos).map_or(false, |s| s == suggested_indent), + "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n", + i+1, + line.slice(..line.len_chars()-1), + suggested_indent, + ); + } + } +} From 9740d4120bd7d899c9fbd6a34951f8ea3fe8c0fd Mon Sep 17 00:00:00 2001 From: Triton171 Date: Fri, 25 Feb 2022 16:48:48 +0100 Subject: [PATCH 14/19] Improve code style in indent.rs. Reuse tree-sitter cursors for indentation queries. --- helix-core/src/indent.rs | 46 +++++++++++++++++++++++----------------- helix-core/src/syntax.rs | 2 +- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index f6efd5107edd..3dbd27470931 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -229,22 +229,17 @@ fn get_first_in_line(mut node: Node, byte_pos: usize, new_line: bool) -> Vec Self { - IndentResult { - indent: 0, - outdent: 0, - } - } +impl Indentation { /// Add some other [IndentResult] to this. /// The added indent should be the total added indent from one line - fn add_line(&mut self, added: &IndentResult) { + fn add_line(&mut self, added: &Indentation) { if added.indent > 0 && added.outdent == 0 { self.indent += 1; } else if added.outdent > 0 && added.indent == 0 { @@ -308,14 +303,13 @@ enum IndentScope { fn query_indents( query: &Query, syntax: &Syntax, + cursor: &mut QueryCursor, text: RopeSlice, range: std::ops::Range, // Position of the (optional) newly inserted line break. // Given as (line, byte_pos) new_line_break: Option<(usize, usize)>, ) -> HashMap> { - // TODO Maybe reuse this cursor? - let mut cursor = QueryCursor::new(); let mut indent_captures: HashMap> = HashMap::new(); cursor.set_byte_range(range); // Iterate over all captures from the query @@ -421,7 +415,8 @@ fn query_indents( } indent_captures .entry(capture.node.id()) - .or_default() + // Most entries only need to contain a single IndentCapture + .or_insert_with(|| Vec::with_capacity(1)) .push(indent_capture); } } @@ -484,13 +479,26 @@ pub fn treesitter_indent_for_pos( } else { None }; - let query_result = query_indents(query, syntax, text, byte_pos..byte_pos + 1, new_line_break); - - let mut result = IndentResult::new(); + let query_result = 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, + byte_pos..byte_pos + 1, + new_line_break, + ); + ts_parser.cursors.push(cursor); + query_result + }); + + let mut result = Indentation::default(); // We always keep track of all the indent changes on one line, in order to only indent once // even if there are multiple "indent" nodes on the same line - let mut indent_for_line = IndentResult::new(); - let mut indent_for_line_below = IndentResult::new(); + let mut indent_for_line = Indentation::default(); + let mut indent_for_line_below = Indentation::default(); loop { let is_first = *first_in_line.last().unwrap(); // Apply all indent definitions for this node @@ -532,9 +540,9 @@ pub fn treesitter_indent_for_pos( indent_for_line_below = indent_for_line; } else { result.add_line(&indent_for_line); - indent_for_line_below = IndentResult::new(); + indent_for_line_below = Indentation::default(); } - indent_for_line = IndentResult::new(); + indent_for_line = Indentation::default(); } node = parent; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 98dcdfa61234..c287452a818f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -445,7 +445,7 @@ impl Loader { pub struct TsParser { parser: tree_sitter::Parser, - cursors: Vec, + pub cursors: Vec, } // could also just use a pool, or a single instance? From 923ee070be8d00c35986fec248ae00ff39126024 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Fri, 25 Feb 2022 17:33:07 +0100 Subject: [PATCH 15/19] Migrate HCL indent query --- runtime/queries/hcl/indents.scm | 13 +++++++++++++ runtime/queries/hcl/indents.toml | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 runtime/queries/hcl/indents.scm delete mode 100644 runtime/queries/hcl/indents.toml diff --git a/runtime/queries/hcl/indents.scm b/runtime/queries/hcl/indents.scm new file mode 100644 index 000000000000..3625641b28e0 --- /dev/null +++ b/runtime/queries/hcl/indents.scm @@ -0,0 +1,13 @@ +[ + (object) + (block) + (tuple) + (for_tuple_expr) + (for_object_expr) +] @indent + +[ + (object_end) + (block_end) + (tuple_end) +] @outdent diff --git a/runtime/queries/hcl/indents.toml b/runtime/queries/hcl/indents.toml deleted file mode 100644 index b0d4a3f0d877..000000000000 --- a/runtime/queries/hcl/indents.toml +++ /dev/null @@ -1,13 +0,0 @@ -indent = [ - "object", - "block", - "tuple", - "for_tuple_expr", - "for_object_expr" -] - -outdent = [ - "object_end", - "block_end", - "tuple_end" -] From 150439308fba6b97a004b2cc355928c6ba074c58 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Fri, 11 Mar 2022 16:11:07 +0100 Subject: [PATCH 16/19] Replace custom loading in indent tests with a designated languages.toml --- helix-core/src/syntax.rs | 6 +-- helix-core/tests/data/indent/languages.toml | 13 ++++++ helix-core/tests/indent.rs | 47 ++++++--------------- 3 files changed, 29 insertions(+), 37 deletions(-) create mode 100644 helix-core/tests/data/indent/languages.toml diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f5f86e59f95f..022c99c3c14c 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -84,7 +84,7 @@ pub struct LanguageConfiguration { // first_line_regex // #[serde(skip)] - pub highlight_config: OnceCell>>, + pub(crate) highlight_config: OnceCell>>, // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 #[serde(skip_serializing_if = "Option::is_none")] pub language_server: Option, @@ -92,9 +92,9 @@ pub struct LanguageConfiguration { pub indent: Option, #[serde(skip)] - pub indent_query: OnceCell>, + pub(crate) indent_query: OnceCell>, #[serde(skip)] - pub textobject_query: OnceCell>, + pub(crate) textobject_query: OnceCell>, #[serde(skip_serializing_if = "Option::is_none")] pub debugger: Option, diff --git a/helix-core/tests/data/indent/languages.toml b/helix-core/tests/data/indent/languages.toml new file mode 100644 index 000000000000..f9cef4942824 --- /dev/null +++ b/helix-core/tests/data/indent/languages.toml @@ -0,0 +1,13 @@ +# This languages.toml should contain definitions for all languages for which we have indent tests +[[language]] +name = "rust" +scope = "source.rust" +injection-regex = "rust" +file-types = ["rs"] +comment-token = "//" +roots = ["Cargo.toml", "Cargo.lock"] +indent = { tab-width = 4, unit = " " } + +[[grammar]] +name = "rust" +source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "a360da0a29a19c281d08295a35ecd0544d2da211" } diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs index c1d31f5ddfb1..ff04d05f5bbe 100644 --- a/helix-core/tests/indent.rs +++ b/helix-core/tests/indent.rs @@ -1,10 +1,8 @@ use helix_core::{ - diagnostic::Severity, indent::{treesitter_indent_for_pos, IndentStyle}, - syntax::{Configuration, IndentationConfiguration, LanguageConfiguration, Loader}, + syntax::Loader, Syntax, }; -use once_cell::sync::OnceCell; use std::path::PathBuf; #[test] @@ -20,38 +18,19 @@ fn test_treesitter_indent_rust_2() { } fn test_treesitter_indent(file_name: &str, lang_scope: &str) { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("tests/data/indent"); - path.push(file_name); - let file = std::fs::File::open(path).unwrap(); - let doc = ropey::Rope::from_reader(file).unwrap(); + let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + test_dir.push("tests/data/indent"); - let loader = Loader::new(Configuration { - language: vec![LanguageConfiguration { - scope: "source.rust".to_string(), - file_types: vec!["rs".to_string()], - shebangs: vec![], - language_id: "Rust".to_string(), - highlight_config: OnceCell::new(), - config: None, - // - injection_regex: None, - roots: vec![], - comment_token: None, - auto_format: false, - diagnostic_severity: Severity::Warning, - grammar: None, - language_server: None, - indent: Some(IndentationConfiguration { - tab_width: 4, - unit: String::from(" "), - }), - indent_query: OnceCell::new(), - textobject_query: OnceCell::new(), - debugger: None, - auto_pairs: None, - }], - }); + let mut test_file = test_dir.clone(); + test_file.push(file_name); + let test_file = std::fs::File::open(test_file).unwrap(); + let doc = ropey::Rope::from_reader(test_file).unwrap(); + + let mut config_file = test_dir; + config_file.push("languages.toml"); + let config = std::fs::read(config_file).unwrap(); + let config = toml::from_slice(&config).unwrap(); + let loader = Loader::new(config); // set runtime path so we can find the queries let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); From 6321937a81705fa3c5dd8fc72be893cebae1866d Mon Sep 17 00:00:00 2001 From: Triton171 Date: Fri, 11 Mar 2022 16:17:22 +0100 Subject: [PATCH 17/19] Update indent query file name for --health command. --- helix-term/src/health.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index f13d35f09c27..2c2438211ce8 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -18,7 +18,7 @@ impl TsFeature { match *self { Self::Highlight => "highlights.scm", Self::TextObject => "textobjects.scm", - Self::AutoIndent => "indents.toml", + Self::AutoIndent => "indents.scm", } } From ae35bc4bcf02a927f372f33f7d70de786b52e952 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Fri, 11 Mar 2022 16:22:32 +0100 Subject: [PATCH 18/19] Fix single-space formatting in indent queries. --- runtime/queries/dart/indents.scm | 6 ++--- runtime/queries/go/indents.scm | 8 +++--- runtime/queries/javascript/indents.scm | 30 +++++++++++------------ runtime/queries/llvm/indents.scm | 4 +-- runtime/queries/nix/indents.scm | 20 +++++++-------- runtime/queries/scala/indents.scm | 34 +++++++++++++------------- runtime/queries/zig/indents.scm | 22 ++++++++--------- 7 files changed, 62 insertions(+), 62 deletions(-) diff --git a/runtime/queries/dart/indents.scm b/runtime/queries/dart/indents.scm index af94ec0c471e..14c6a375889a 100644 --- a/runtime/queries/dart/indents.scm +++ b/runtime/queries/dart/indents.scm @@ -14,7 +14,7 @@ ] @indent [ - "}" - "]" - ")" + "}" + "]" + ")" ] @outdent diff --git a/runtime/queries/go/indents.scm b/runtime/queries/go/indents.scm index 881db4c2750e..d75417d97a39 100644 --- a/runtime/queries/go/indents.scm +++ b/runtime/queries/go/indents.scm @@ -19,8 +19,8 @@ ] @indent [ - "case" - "}" - "]" - ")" + "case" + "}" + "]" + ")" ] @outdent diff --git a/runtime/queries/javascript/indents.scm b/runtime/queries/javascript/indents.scm index 539d54549b34..a4237e59982c 100644 --- a/runtime/queries/javascript/indents.scm +++ b/runtime/queries/javascript/indents.scm @@ -1,22 +1,22 @@ [ - (array) - (object) - (arguments) - (formal_parameters) + (array) + (object) + (arguments) + (formal_parameters) - (statement_block) - (object_pattern) - (class_body) - (named_imports) + (statement_block) + (object_pattern) + (class_body) + (named_imports) - (binary_expression) - (return_statement) - (template_substitution) - (export_clause) + (binary_expression) + (return_statement) + (template_substitution) + (export_clause) ] @indent [ - "}" - "]" - ")" + "}" + "]" + ")" ] @outdent diff --git a/runtime/queries/llvm/indents.scm b/runtime/queries/llvm/indents.scm index 7065bee19a42..293eeebf4133 100644 --- a/runtime/queries/llvm/indents.scm +++ b/runtime/queries/llvm/indents.scm @@ -1,6 +1,6 @@ [ - (function_body) - (instruction) + (function_body) + (instruction) ] @indent "}" @outdent diff --git a/runtime/queries/nix/indents.scm b/runtime/queries/nix/indents.scm index 06e811408dd8..0790ce2919ff 100644 --- a/runtime/queries/nix/indents.scm +++ b/runtime/queries/nix/indents.scm @@ -1,15 +1,15 @@ [ - ; "function", - (bind) - (assert) - (with) - (let) - (if) + ; "function", + (bind) + (assert) + (with) + (let) + (if) - (attrset) - (list) - (indented_string) - (parenthesized) + (attrset) + (list) + (indented_string) + (parenthesized) ] @indent [ diff --git a/runtime/queries/scala/indents.scm b/runtime/queries/scala/indents.scm index e3fec2c18cbd..3449cfa7a965 100644 --- a/runtime/queries/scala/indents.scm +++ b/runtime/queries/scala/indents.scm @@ -1,22 +1,22 @@ [ - (block) - (arguments) - (parameter) - (class_definition) - (trait_definition) - (object_definition) - (function_definition) - (val_definition) - (import_declaration) - (while_expression) - (do_while_expression) - (for_expression) - (try_expression) - (match_expression) + (block) + (arguments) + (parameter) + (class_definition) + (trait_definition) + (object_definition) + (function_definition) + (val_definition) + (import_declaration) + (while_expression) + (do_while_expression) + (for_expression) + (try_expression) + (match_expression) ] @indent [ - "}" - "]" - ")" + "}" + "]" + ")" ] @outdent diff --git a/runtime/queries/zig/indents.scm b/runtime/queries/zig/indents.scm index ad31b01f38ce..af25a9c3c567 100644 --- a/runtime/queries/zig/indents.scm +++ b/runtime/queries/zig/indents.scm @@ -1,16 +1,16 @@ [ - (Block) - (BlockExpr) - (ContainerDecl) - (SwitchExpr) - (AssignExpr) - (ErrorUnionExpr) - (Statement) - (InitList) + (Block) + (BlockExpr) + (ContainerDecl) + (SwitchExpr) + (AssignExpr) + (ErrorUnionExpr) + (Statement) + (InitList) ] @indent [ - "}" - "]" - ")" + "}" + "]" + ")" ] @outdent From 3055f63f62cd0786fe6898cf087025708005fd16 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Fri, 11 Mar 2022 17:33:56 +0100 Subject: [PATCH 19/19] Add explanation for unwrapping. --- helix-core/src/indent.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 3dbd27470931..529139b81e25 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -500,6 +500,8 @@ pub fn treesitter_indent_for_pos( let mut indent_for_line = Indentation::default(); let mut indent_for_line_below = Indentation::default(); loop { + // 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 if let Some(definitions) = query_result.get(&node.id()) {