diff --git a/book/src/guides/indent.md b/book/src/guides/indent.md index 769b9bc8..6b3a4c05 100644 --- a/book/src/guides/indent.md +++ b/book/src/guides/indent.md @@ -2,16 +2,17 @@ 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 (see [Indent queries](#indent-queries)). -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). +`runtime/queries/{language}/indents.scm`. -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. See [Capture types](#capture-types). +The indent level of a line is **the number of `@indent` scopes that contain +it**. An `@indent` capture on a node opens a scope spanning the lines *after* +the node's first line through its last line; a line is indented once for every +such scope wrapping it. (A few capture types adjust this — `@outdent` cancels a +level, `@align` aligns to a column, etc. (see [Capture types](#capture-types).) + +Note that it matters where these scopes *begin*: multiple scopes that open on +the same physical line only increase the indent by 1 (the same-line rule). See +[Capture types](#capture-types). By default, Helix uses the `hybrid` indentation heuristic. This means that indent queries are not used to compute the expected absolute indentation of a @@ -61,26 +62,30 @@ fn need_hero(some_hero: Hero, life: Life) -> { // ←─╮ } // ←──────────────────────────────────────────────╯ ``` -From this starting node, the syntax tree is traversed up until the root node. -Each indent capture is collected along the way, and then combined according to -their [capture types](#capture-types) and [scopes](#scopes) to a final indent -level for the line. +From this starting node, the syntax tree is walked up to the root, collecting +every `@indent`/`@outdent`/`@align`/... capture on an ancestor. Each `@indent` +ancestor whose scope *contains* the line counts for one indent level (scopes +opening on the same line collapse to one); the [capture types](#capture-types) +describe the adjustments. ### Capture types -- `@indent` (default scope `tail`): - Increase the indent level by 1. Multiple occurrences in the same line *do not* - 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. -- `@indent.always` (default scope `tail`): - Increase the indent level by 1. Multiple occurrences on the same line *do* - stack. The final indent level is `@indent.always` – `@outdent.always`. If - an `@indent` and an `@indent.always` are on the same line, the `@indent` is - ignored. -- `@outdent.always` (default scope `all`): - Decrease the indent level by 1. The same rules as for `@indent.always` apply. +- `@indent`: + Open an indent scope on this node — every line it contains is indented one + more level. Scopes that open on the same line collapse: multiple `@indent` + beginning on one line only add 1. A node captured by *both* `@indent` and + `@outdent` contributes nothing (its level is cancelled) — used e.g. for a + nested `else if` that should not stack a second level. + By default the scope opens at the node's own first line; see the + [`header`](#scope-header) scope to open it at the parent (header) line instead. +- `@outdent`: + Decrease by 1 the indent of the line on which this (usually a closing token + like `}`/`)`/`]`, or a keyword like `else`) begins. +- `@indent.always`: + Like `@indent` but does *not* collapse — multiple on the same line each add a + level. The net level contribution is `@indent.always` − `@outdent.always`. +- `@outdent.always`: + Like `@outdent` but stacks, the counterpart to `@indent.always`. - `@align` (default scope `all`): Align everything inside this node to some anchor. The anchor is given by the start of the node captured by `@anchor` in the same pattern. @@ -131,11 +136,43 @@ fn shout(things: Vec) { ["}" ")"] @outdent ``` -Note how on the second line, we have two blocks begin on the same line. In this -case, since both captures occur on the same line, they are combined and only -result in a net increase of 1. Also note that the closing `}`s are part of the -`@indent` captures, but the 3 `@outdent`s also combine into 1 and result in that -line losing one indent level. +Note how on the second line two blocks open on the same line: since both scopes +*begin* on that line they collapse, for a net increase of 1. On the last line, +the three block scopes all contain it, but it begins with three `@outdent` `}` +tokens that cancel them, so the line lands back at the enclosing level. + +#### Same-line collapse + +The collapse above is a deliberate, load-bearing invariant: + +> A line is indented by **one level per physical line on which a containing +> `@indent` scope opens**, not one level per scope. Several `@indent` scopes +> that *open* on the same line together add a single level. + +This is what makes **method/builder chains flatten instead of staircasing**. +Grammars typically nest a chain so each `.method()` link is a `call` inside a +`member_expression` inside the previous link, and several of those nodes *begin* +on the receiver's line: + +```rust +let x = thing // ← chain opens here + .foo() // each link aligned one level in, + .bar() // not progressively deeper + .baz(); +``` + +```scm +(call_expression) @indent +``` + +Even though many `call_expression`/`member_expression` scopes contain the +`.bar()` line, they all *open* on the `thing` line, so they collapse to a single +level and the continuation lines line up. Without the collapse, every link would +add a level and the chain would stair-step to the right. + +If you instead want each scope to count even when several open on one line (for +example YAML's "list item *and* map both start on the same line") opt out with +`@indent.always` (described below), which does not collapse. #### `@extend` / `@extend.prevent-once` @@ -257,29 +294,31 @@ To help, we need to signal an end to the extension. We can do this with #### Brace-less bodies -A brace-less single-statement body: `if (cond)` with its statement on the next -line and no `{}` is a *following sibling* of the header, so the upward -traversal from the line above never reaches it. Capture the body directly and -give it the `all` scope: +A brace-less single-statement body — `if (cond)` with its statement on the next +line and no `{}` — is a body the indent must wrap, but the body node's *own* +first line is the line that needs the indent (there is no separate opening line). +Capture the body and give it the [`header`](#scope-header) scope, which opens the +scope at the **header** (the captured node's parent) line instead of the body's +own line, so the body line is contained: ```scm (if_statement consequence: (_) @indent (#not-kind-eq? @indent "compound_statement") - (#set! "scope" "all")) + (#set! "scope" "header")) (while_statement body: (_) @indent (#not-kind-eq? @indent "compound_statement") - (#set! "scope" "all")) + (#set! "scope" "header")) ``` -When a new line is typed right after the header, Helix descends into the -field-named body (`body`, `consequence`, or `alternative`) that the line opens, -so the body's own `@indent` governs it — no wrapper node or `@extend` is needed. -The `#not-kind-eq?` guard skips the braced form, which the surrounding block -already indents. Give `else` and `do .. while` their own pattern (on the -`alternative` / `body` field) so the trailing `else` / `while` keyword line is -not indented along with the body. +The query already names exactly the brace-less body (the `consequence:` / +`body:` field, with `#not-kind-eq?` skipping the braced form the surrounding +block indents), so the engine just opens that capture's scope at its parent — it +does not itself inspect node kinds or field names. This works for a multi-line +brace-less body too (e.g. one returning a lambda). Give `else` and `do / while` +their own pattern (on the `alternative` / `body` field) so the trailing `else` / +`while` keyword line is not indented along with the body. #### `@indent.always` / `@outdent.always` @@ -362,46 +401,29 @@ The captures given by the 2 arguments must/must not start on the same line. - `#one-line?`/`#not-one-line?`: The captures given by the fist argument must/must span a total of one line. -### 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): +### The `header` scope -- `tail`: -This scope applies to everything except for the first line of the -captured node. -- `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. +By default an `@indent` scope opens at the captured node's own first line, so the +lines *inside* it are indented and its first line is not. Sometimes the node you +must capture *is* the line that needs indenting — a brace-less body such as the +statement after `if (cond)` (see [Brace-less bodies](#brace-less-bodies)). For +this, set `scope` to `header`: -For example, imagine we have the following function - -```rust -fn aha() { // ←─────────────────────────────────────╮ - let take = "on me"; // ←──────────────╮ scope: │ - let take = "me on"; // ├─ "tail" ├─ (block) @indent - let ill = be_gone_days(1 || 2); // │ │ -} // ←───────────────────────────────────┴──────────┴─ "}" @outdent - // scope: "all" +```scm +(if_statement + consequence: (_) @indent + (#not-kind-eq? @indent "compound_statement") + (#set! "scope" "header")) ``` -We can write the following query with the `#set!` declaration: - - ```scm - ((block) @indent - (#set! "scope" "tail")) - ("}" @outdent - (#set! "scope" "all")) - ``` - -As we can see, the "tail" scope covers the node, except for the first line. -Everything up to and including the closing brace gets an indent level of 1. -Then, on the closing brace, we encounter an outdent with a scope of "all", which -means the first line is included, and the indent level is cancelled out on this -line. (Note these scopes are the defaults for `@indent` and `@outdent`—they are -written explicitly for demonstration.) +`header` opens the scope at the captured node's **parent** (the `if_statement` +header) line instead of the node's own line, so the body's first line is +contained and indented. The query selects exactly the body, so the engine just +honours the annotation — it does not match on node kinds or field names itself. + +(Older queries used `tail` / `all` scopes to control whether a node's first line +was included; under the containment model those are no longer needed and have +been removed. `header` is the one scope the engine reads.) ## Testing diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 61c659c9..2fa5a2f8 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,4 +1,7 @@ -use std::{borrow::Cow, collections::HashMap}; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, +}; use helix_stdx::rope::RopeSliceExt; use tree_house::TREE_SITTER_MATCH_LIMIT; @@ -290,21 +293,6 @@ fn add_indent_level( } } -/// Return true if only whitespace comes before the node on its line. -/// If given, new_line_byte_pos is treated the same way as any existing newline. -fn is_first_in_line(node: &Node, text: RopeSlice, new_line_byte_pos: Option) -> bool { - let line = text.byte_to_line(node.start_byte() as usize); - let mut line_start_byte_pos = text.line_to_byte(line) as u32; - if let Some(pos) = new_line_byte_pos { - if line_start_byte_pos < pos && pos <= node.start_byte() { - line_start_byte_pos = pos; - } - } - text.byte_slice(line_start_byte_pos as usize..node.start_byte() as usize) - .chars() - .all(|c| c.is_whitespace()) -} - #[derive(Debug, Default)] pub struct IndentQueryPredicates { not_kind_eq: Vec<(Capture, Box)>, @@ -362,7 +350,9 @@ impl IndentQueryPredicates { #[derive(Debug)] pub struct IndentQuery { query: Query, - properties: HashMap, + /// Patterns carrying `(#set! "scope" "header")` — the only indent scope the + /// containment engine reads. + header_patterns: HashSet, predicates: HashMap, indent_capture: Option, indent_always_capture: Option, @@ -377,21 +367,20 @@ pub struct IndentQuery { impl IndentQuery { pub fn new(grammar: Grammar, source: &str) -> Result { - let mut properties = HashMap::new(); + let mut header_patterns = HashSet::new(); let mut predicates: HashMap = HashMap::new(); let query = Query::new(grammar, source, |pattern, predicate| match predicate { UserPredicate::SetProperty { key: "scope", val } => { - let scope = match val { - Some("all") => IndentScope::All, - Some("tail") => IndentScope::Tail, + match val { + Some("header") => { + header_patterns.insert(pattern); + } Some(other) => { return Err(format!("unknown scope (#set! scope \"{other}\")").into()) } None => return Err("missing scope value (#set! scope ...)".into()), }; - properties.insert(pattern, scope); - Ok(()) } UserPredicate::Other(predicate) => { @@ -436,7 +425,7 @@ impl IndentQuery { })?; Ok(Self { - properties, + header_patterns, predicates, indent_capture: query.get_capture("indent"), indent_always_capture: query.get_capture("indent.always"), @@ -472,56 +461,6 @@ pub struct Indentation<'a> { impl<'a> Indentation<'a> { /// Add some other [Indentation] to this. - /// The added indent should be the total added indent from one line. - /// Indent should always be added starting from the bottom (or equivalently, the innermost tree-sitter node). - fn add_line(&mut self, added: Indentation<'a>) { - // Align overrides the indent from outer scopes. - if self.align.is_some() { - return; - } - if added.align.is_some() { - self.align = added.align; - return; - } - self.indent += added.indent; - self.indent_always += added.indent_always; - self.outdent += added.outdent; - self.outdent_always += added.outdent_always; - } - - /// Add an indent capture to this indent. - /// Only captures that apply to the same line should be added together in this way (otherwise use `add_line`) - /// and the captures should be added starting from the innermost tree-sitter node (currently this only matters - /// if multiple `@align` patterns occur on the same line). - fn add_capture(&mut self, added: IndentCaptureType<'a>) { - match added { - IndentCaptureType::Indent => { - if self.indent_always == 0 { - self.indent = 1; - } - } - IndentCaptureType::IndentAlways => { - // any time we encounter an `indent.always` on the same line, we - // want to cancel out all regular indents - self.indent_always += 1; - self.indent = 0; - } - IndentCaptureType::Outdent => { - if self.outdent_always == 0 { - self.outdent = 1; - } - } - IndentCaptureType::OutdentAlways => { - self.outdent_always += 1; - self.outdent = 0; - } - IndentCaptureType::Align(align) => { - if self.align.is_none() { - self.align = Some(align); - } - } - } - } fn net_indent(&self) -> isize { (self.indent + self.indent_always) as isize - ((self.outdent + self.outdent_always) as isize) @@ -565,7 +504,11 @@ impl<'a> Indentation<'a> { #[derive(Debug)] struct IndentCapture<'a> { capture_type: IndentCaptureType<'a>, - scope: IndentScope, + /// `(#set! "scope" "header")`: open this `@indent`'s scope at the captured + /// node's *header* (its parent's start line) instead of the node's own first + /// line, so a brace-less body (`if (c)\n stmt;`) whose own first line needs + /// the indent is contained. + header: bool, } #[derive(Debug, Clone, PartialEq)] enum IndentCaptureType<'a> { @@ -577,26 +520,6 @@ enum IndentCaptureType<'a> { Align(RopeSlice<'a>), } -impl IndentCaptureType<'_> { - fn default_scope(&self) -> IndentScope { - match self { - IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail, - IndentCaptureType::Outdent | IndentCaptureType::OutdentAlways => IndentScope::All, - IndentCaptureType::Align(_) => IndentScope::All, - } - } -} -/// 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(Debug, Clone, Copy)] -enum IndentScope { - /// The indent applies to the whole node - All, - /// The indent applies to everything except for the first line of the node - Tail, -} - /// A capture from the indent query which does not define an indent but extends /// the range of a node. This is used before the indent is calculated. #[derive(Debug)] @@ -700,14 +623,9 @@ fn query_indents<'a>( }; // Apply additional settings for this capture - let scope = query - .properties - .get(&m.pattern()) - .copied() - .unwrap_or_else(|| capture_type.default_scope()); let indent_capture = IndentCapture { capture_type, - scope, + header: query.header_patterns.contains(&m.pattern()), }; added_indent_captures.push((node_id, indent_capture)) } @@ -812,21 +730,21 @@ fn extend_nodes<'a>( /// When a newline is typed after a header (`while (c)`, `if x:`), the body it /// opens begins on the *next* line — a following sibling the upward indent walk -/// never reaches, which is why brace-less bodies historically needed wrapper -/// `@indent` rules or `@extend`. Walk up from the deepest preceding node; if an -/// ancestor exposes a body field (`body`/`consequence`/`alternative`) that -/// begins at/after the cursor, return it so the indent walk can start *inside* -/// the body and its own scope governs the new line. +/// never reaches, which is why brace-less bodies need either a wrapper `@indent` +/// rule or descent into the body before the containment walk starts. /// -/// Matching on a body *field* (not just any following sibling) is what -/// distinguishes a real body from a peer statement: a statement list / block -/// does not field-name its children, so `foo();` followed by `bar();` finds no -/// field and is correctly left alone. -fn following_body_for_new_line<'a>( +/// Walk up from the deepest preceding node and return the immediate next sibling +/// of the first ancestor whose end lies before the cursor. The caller then +/// decides whether to actually descend — the signal is the query's +/// `(#set! "scope" "header")` annotation on that sibling, which is exactly how +/// brace-less body rules already mark their body capture (c/java/nix and parts +/// of ecma). This keeps tree-sitter field names out of the engine: the query +/// alone says what is a body. A statement-list child is not header-scoped in any +/// query, so `foo();` then `bar();` correctly finds no descent target. +fn candidate_body_for_new_line<'a>( deepest_preceding: &Node<'a>, byte_pos: u32, ) -> Option> { - const BODY_FIELDS: [&str; 3] = ["body", "consequence", "alternative"]; let mut node = deepest_preceding.clone(); while let Some(parent) = node.parent() { // Only consider ancestors that lie entirely before the cursor: those are @@ -836,21 +754,17 @@ fn following_body_for_new_line<'a>( if node.end_byte() > byte_pos { break; } - // The body the new line opens is the field-named sibling immediately - // after this header node (`while (c)` -> body, `if x:` -> consequence). - // Requiring the *immediate* next sibling avoids jumping to a later body - // such as an `else` arm while the cursor is still in the consequence. + // The body the new line opens is the sibling immediately after this + // header node (`while (c)` -> body, `if x:` -> consequence). Requiring + // the *immediate* next sibling avoids jumping to a later body such as + // an `else` arm while the cursor is still in the consequence. let mut cursor = parent.walk(); if cursor.goto_first_child() { loop { if cursor.node().id() == node.id() { if cursor.goto_next_sibling() { let child = cursor.node(); - if child.start_byte() >= byte_pos - && cursor - .field_name() - .is_some_and(|f| BODY_FIELDS.contains(&f)) - { + if child.start_byte() >= byte_pos { return Some(child); } } @@ -884,7 +798,7 @@ fn init_indent_query<'a, 'b>( // computation. It may change if some preceding node is extended let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?; - let (query_result, deepest_preceding) = { + let (query_result, deepest_preceding, candidate_body) = { // The query range should intersect with all nodes directly preceding // the position of the indent query in case one of them is extended. let mut deepest_preceding = None; // The deepest node preceding the indent query position @@ -900,25 +814,43 @@ fn init_indent_query<'a, 'b>( } prec }); + // When typing a newline, the body the new line opens is a sibling past + // the cursor — extend the query range to cover it so its capture (and + // in particular its `(#set! "scope" "header")` annotation) is present + // in the result, the signal we use to drive descent. + let candidate_body = new_line_byte_pos + .and(deepest_preceding.as_ref()) + .and_then(|dp| candidate_body_for_new_line(dp, byte_pos)); + let upper = candidate_body + .as_ref() + .map(|b| b.end_byte()) + .unwrap_or(byte_pos + 1); let query_range = deepest_preceding .as_ref() - .map(|prec| prec.byte_range().end - 1..byte_pos + 1) - .unwrap_or(byte_pos..byte_pos + 1); + .map(|prec| prec.byte_range().end - 1..upper) + .unwrap_or(byte_pos..upper); let query_result = query_indents(query, root, text, query_range, new_line_byte_pos); - (query_result, deepest_preceding) + (query_result, deepest_preceding, candidate_body) }; let extend_captures = query_result.extend_captures; // When typing a newline, descend into the body the new line opens so its own // scope governs the indent (structural replacement for wrapper/`@extend` - // rules on delimited and brace-less bodies). Otherwise fall back to `@extend`. - let descended = new_line_byte_pos.is_some() - && deepest_preceding - .as_ref() - .and_then(|dp| following_body_for_new_line(dp, byte_pos)) - .map(|body| node = body) - .is_some(); + // rules on delimited and brace-less bodies). The query — via its + // `(#set! "scope" "header")` annotation on the body capture — is the single + // source of truth for whether the candidate sibling is actually a body to + // descend into. Otherwise fall back to `@extend`. + let descended = candidate_body + .as_ref() + .filter(|body| { + query_result + .indent_captures + .get(&body.id()) + .is_some_and(|defs| defs.iter().any(|c| c.header)) + }) + .map(|body| node = body.clone()) + .is_some(); // Check for extend captures, potentially changing the node that the indent calculation starts with if let Some(deepest_preceding) = deepest_preceding { @@ -1054,8 +986,9 @@ pub fn treesitter_indent_for_pos<'a>( ..Default::default() }); } - let new_line_byte_pos = new_line.then_some(byte_pos); - let (mut node, mut indent_captures) = init_indent_query( + // Compute the indent by *scope containment*: the level of a line is the + // number of `@indent` scopes that contain it (see `containment_accounting`). + let mut result = containment_accounting( query, &root, text, @@ -1063,79 +996,154 @@ pub fn treesitter_indent_for_pos<'a>( indent_width, line, byte_pos, + new_line, + )?; + // The injected walk above is relative to the injection's own tree root (the + // embedded code starts at level 0). Shift it by the indent of the injection's + // first content line so it sits where the host language placed the block. + if injected.is_some() { + result.indent += injection_base_level(syntax, text, byte_pos, tab_width, indent_width); + } + Some(result) +} + +/// Compute indentation by *scope containment*. +/// +/// Each node carrying an `@indent`/`@indent.always` capture defines a scope +/// spanning the lines after its start up to its end. The indent level of a line +/// is the number of such scopes containing it (collapsing scopes that open on the +/// same physical line to one level), minus any `@outdent` whose token begins the line. +/// `@align`/`@extend`/`@opaque` are kept as overlays. +fn containment_accounting<'a>( + query: &IndentQuery, + root: &Node<'a>, + text: RopeSlice<'a>, + tab_width: usize, + indent_width: usize, + line: usize, + byte_pos: u32, + new_line: bool, +) -> Option> { + let new_line_byte_pos = new_line.then_some(byte_pos); + // Reuse the standard setup: this applies @extend repositioning / body-descent + // and returns the per-node capture map (all ancestors of the cursor intersect + // the query range, so their captures are present). + let (start_node, captures) = init_indent_query( + query, + root, + text, + tab_width, + indent_width, + line, + byte_pos, new_line_byte_pos, )?; - let mut result = Indentation::default(); - // We always keep track of all the indent changes on one line, in order to only indent once - // even if there are multiple "indent" nodes on the same line - let mut indent_for_line = Indentation::default(); - let mut indent_for_line_below = Indentation::default(); + // The line whose indent we are computing (post-insertion coordinates). + let target_line = line + new_line as usize; + + let mut indent_levels: usize = 0; + let mut indent_always: usize = 0; + let mut outdent: usize = 0; + let mut outdent_always: usize = 0; + let mut align: Option> = None; + // Same-line collapse: a scope contributes at most one level per physical line + // it opens on. + let mut counted_start_lines: Vec = Vec::new(); + let mut node = start_node; loop { - let is_first = is_first_in_line(&node, text, new_line_byte_pos); - - // Apply all indent definitions for this node. - // Since we only iterate over each node once, we can remove the - // corresponding captures from the HashMap to avoid cloning them. - if let Some(definitions) = indent_captures.remove(&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); - } + if let Some(defs) = captures.get(&node.id()) { + // Aggregate this node's captures. + let mut has_indent = false; + let mut has_indent_always = false; + let mut has_outdent = false; + let mut has_outdent_always = false; + let mut header_scoped = false; + let mut node_align: Option> = None; + for def in defs { + match def.capture_type { + IndentCaptureType::Indent => { + has_indent = true; + header_scoped |= def.header; } - IndentScope::Tail => { - indent_for_line_below.add_capture(definition.capture_type); + IndentCaptureType::IndentAlways => { + has_indent_always = true; + header_scoped |= def.header; } + IndentCaptureType::Outdent => has_outdent = true, + IndentCaptureType::OutdentAlways => has_outdent_always = true, + IndentCaptureType::Align(a) => node_align = Some(a), } } - } - if let Some(parent) = node.parent() { - let node_line = get_node_start_line(text, &node, new_line_byte_pos); - let parent_line = get_node_start_line(text, &parent, new_line_byte_pos); - - if node_line != parent_line { - // Don't add indent for the line below the line of the query - if node_line < line + (new_line as usize) { - result.add_line(indent_for_line_below); + let node_start = get_node_start_line(text, &node, new_line_byte_pos); + let end = get_node_end_line(text, &node, new_line_byte_pos); + // `scope "header"` (set by the query on a brace-less body rule such as + // `(if_statement consequence: (_) @indent (#set! scope "header"))`) + // opens the scope at the *header* — the captured node's parent — so the + // body's own first line is contained. The query selects the body node, + // so its parent is the header by construction. + let start = if header_scoped { + node.parent() + .map(|p| get_node_start_line(text, &p, new_line_byte_pos)) + .unwrap_or(node_start) + } else { + node_start + }; + // A scope contains the target line if it opens before it and closes on + // or after it. + let contains = start < target_line && target_line <= end; + // A token-style outdent (`}`, `else`, `case`) sits on the line it + // dedents. + let opens_target = node_start == target_line; + + if (has_indent || has_indent_always) && (has_outdent || has_outdent_always) { + // A node carrying both indent and outdent (e.g. swift's nested + // else-if `(if_statement (if_statement) @outdent)`) cancels its own + // level: it must not create a scope. + } else { + if has_indent && contains && !counted_start_lines.contains(&start) { + counted_start_lines.push(start); + indent_levels += 1; } - - if node_line == parent_line + 1 { - indent_for_line_below = indent_for_line; - } else { - result.add_line(indent_for_line); - indent_for_line_below = Indentation::default(); + if has_indent_always && contains { + indent_always += 1; + } + if has_outdent && opens_target { + outdent += 1; + } + if has_outdent_always && opens_target { + outdent_always += 1; } - - indent_for_line = Indentation::default(); } - - node = parent; - } else { - // Only add the indentation for the line below if that line - // is not after the line that the indentation is calculated for. - let node_start_line = text.byte_to_line(node.start_byte() as usize); - if node_start_line < line - || (new_line && node_start_line == line && node.start_byte() < byte_pos) - { - result.add_line(indent_for_line_below); + // Innermost containing alignment wins (first seen on the upward walk). + if let Some(a) = node_align { + if contains && align.is_none() { + align = Some(a); + } } - result.add_line(indent_for_line); - break; + } + match node.parent() { + Some(parent) => node = parent, + None => break, } } - // The injected walk above is relative to the injection's own tree root (the - // embedded code starts at level 0). Shift it by the indent of the injection's - // first content line so it sits where the host language placed the block. - if injected.is_some() { - result.indent += injection_base_level(syntax, text, byte_pos, tab_width, indent_width); + + // `@align` is an absolute alignment (to the anchor column); the containment + // levels it spans are already encoded in that column, so don't stack them on + // top. + if align.is_some() { + indent_levels = 0; + indent_always = 0; } - Some(result) + Some(Indentation { + indent: indent_levels, + indent_always, + outdent, + outdent_always, + align, + }) } /// The indent level (in the host document) of the first content line of the @@ -1377,127 +1385,6 @@ mod test { ); } - #[test] - fn add_capture() { - let indent = || Indentation { - indent: 1, - ..Default::default() - }; - let indent_always = || Indentation { - indent_always: 1, - ..Default::default() - }; - let outdent = || Indentation { - outdent: 1, - ..Default::default() - }; - let outdent_always = || Indentation { - outdent_always: 1, - ..Default::default() - }; - - fn add_capture<'a>( - mut indent: Indentation<'a>, - capture: IndentCaptureType<'a>, - ) -> Indentation<'a> { - indent.add_capture(capture); - indent - } - - // adding an indent to no indent makes an indent - assert_eq!( - indent(), - add_capture(Indentation::default(), IndentCaptureType::Indent) - ); - assert_eq!( - indent_always(), - add_capture(Indentation::default(), IndentCaptureType::IndentAlways) - ); - assert_eq!( - outdent(), - add_capture(Indentation::default(), IndentCaptureType::Outdent) - ); - assert_eq!( - outdent_always(), - add_capture(Indentation::default(), IndentCaptureType::OutdentAlways) - ); - - // adding an indent to an already indented has no effect - assert_eq!(indent(), add_capture(indent(), IndentCaptureType::Indent)); - assert_eq!( - outdent(), - add_capture(outdent(), IndentCaptureType::Outdent) - ); - - // adding an always to a regular makes it always - assert_eq!( - indent_always(), - add_capture(indent(), IndentCaptureType::IndentAlways) - ); - assert_eq!( - outdent_always(), - add_capture(outdent(), IndentCaptureType::OutdentAlways) - ); - - // adding an always to an always is additive - assert_eq!( - Indentation { - indent_always: 2, - ..Default::default() - }, - add_capture(indent_always(), IndentCaptureType::IndentAlways) - ); - assert_eq!( - Indentation { - outdent_always: 2, - ..Default::default() - }, - add_capture(outdent_always(), IndentCaptureType::OutdentAlways) - ); - - // adding regular to always should be associative - assert_eq!( - Indentation { - indent_always: 1, - ..Default::default() - }, - add_capture( - add_capture(indent(), IndentCaptureType::Indent), - IndentCaptureType::IndentAlways - ) - ); - assert_eq!( - Indentation { - indent_always: 1, - ..Default::default() - }, - add_capture( - add_capture(indent(), IndentCaptureType::IndentAlways), - IndentCaptureType::Indent - ) - ); - assert_eq!( - Indentation { - outdent_always: 1, - ..Default::default() - }, - add_capture( - add_capture(outdent(), IndentCaptureType::Outdent), - IndentCaptureType::OutdentAlways - ) - ); - assert_eq!( - Indentation { - outdent_always: 1, - ..Default::default() - }, - add_capture( - add_capture(outdent(), IndentCaptureType::OutdentAlways), - IndentCaptureType::Outdent - ) - ); - } - #[test] fn test_relative_indent() { let indent_style = IndentStyle::Spaces(4); diff --git a/languages.toml b/languages.toml index 14b0bb12..ec3cc3db 100644 --- a/languages.toml +++ b/languages.toml @@ -2537,6 +2537,7 @@ scope = "source.swift" injection-regex = "swift" file-types = ["swift", "swiftinterface"] roots = [ "Package.swift" ] +indent = { tab-width = 2, unit = " " } comment-token = "//" block-comment-tokens = { start = "/*", end = "*/" } formatter = { command = "swift-format" } @@ -4329,6 +4330,7 @@ injection-regex = "tcl" file-types = [ "tcl" ] shebangs = [ "tclsh", "tclish", "jimsh", "wish" ] comment-token = '#' +indent = { tab-width = 4, unit = " " } [[grammar]] name = "tcl" diff --git a/runtime/queries/c/indents.scm b/runtime/queries/c/indents.scm index 56d76359..0fa4b9b7 100644 --- a/runtime/queries/c/indents.scm +++ b/runtime/queries/c/indents.scm @@ -21,33 +21,33 @@ (if_statement consequence: (_) @indent (#not-kind-eq? @indent "compound_statement") - (#set! "scope" "all")) + (#set! "scope" "header")) (while_statement body: (_) @indent (#not-kind-eq? @indent "compound_statement") - (#set! "scope" "all")) + (#set! "scope" "header")) (do_statement body: (_) @indent (#not-kind-eq? @indent "compound_statement") - (#set! "scope" "all")) + (#set! "scope" "header")) (for_statement body: (_) @indent (#not-kind-eq? @indent "compound_statement") - (#set! "scope" "all")) + (#set! "scope" "header")) (parameter_list . (parameter_declaration) @anchor - (#set! "scope" "tail")) @align + ) @align (argument_list . (_) @anchor - (#set! "scope" "tail")) @align + ) @align ; These are a bit opinionated since some people just indent binary/ternary expressions spanning multiple lines. ; Since they are only triggered when a newline is inserted into an already complete binary/ternary expression, ; this should happen rarely, so it is not a big deal either way. ; Additionally, adding these queries has the advantage of preventing such continuation lines from being used -; as the baseline when the `hybrid` indent heuristic is used (which is desirable since their indentation is so inconsistent). +; as the baseline when the `hybrid` indent heuristic is used (which is desirable since their indentation is so inconsistent). (binary_expression - (#set! "scope" "tail")) @anchor @align + ) @anchor @align (conditional_expression "?" @anchor - (#set! "scope" "tail")) @align + ) @align diff --git a/runtime/queries/cairo/indents.scm b/runtime/queries/cairo/indents.scm index 8c6abd10..ecd1462c 100644 --- a/runtime/queries/cairo/indents.scm +++ b/runtime/queries/cairo/indents.scm @@ -34,42 +34,42 @@ (_) @expr-start right: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (compound_assignment_expr . (_) @expr-start right: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (let_declaration "let" @expr-start value: (_) @indent alternative: (_)? @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (let_condition . (_) @expr-start value: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (if_expression . (_) @expr-start condition: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (field_pattern . (_) @expr-start pattern: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) ; Indent type aliases that span multiple lines, similar to ; regular assignment expressions @@ -78,7 +78,7 @@ (_) @expr-start type: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) ; Some field expressions where the left part is a multiline expression are not @@ -102,7 +102,6 @@ arguments: (_) @outdent ) - ; Indent if guards in patterns. ; Since the tree-sitter grammar doesn't create a node for the if expression, ; it's not possible to do this correctly in all cases. Indenting the tail of the @@ -130,5 +129,5 @@ . (_) @indent (#not-same-line? @in @indent) - (#set! "scope" "all") -) + +) diff --git a/runtime/queries/clojure/indents.scm b/runtime/queries/clojure/indents.scm index 115eea1f..201ab052 100644 --- a/runtime/queries/clojure/indents.scm +++ b/runtime/queries/clojure/indents.scm @@ -2,7 +2,7 @@ ; Exclude literals and special keywords that have different indentation rules. (list_lit . (_) @first . (_) @anchor (#same-line? @first @anchor) - (#set! "scope" "tail") + (#not-kind-eq? @first "bool_lit") (#not-kind-eq? @first "nil_lit") (#not-kind-eq? @first "str_lit") @@ -13,11 +13,11 @@ ; If the first element in a list is also a list and on a line by itself, ; the outer list is aligned to it (list_lit . (list_lit) @anchor . - (#set! "scope" "tail")) @align + ) @align (list_lit . (list_lit) @anchor . (_) @second (#not-same-line? @anchor @second) - (#set! "scope" "tail")) @align + ) @align ; If the first element in a list is not a list and on a line by itself, ; indent the list body by one level @@ -40,7 +40,7 @@ ; If the first element is a literal, align the list to it (list_lit . [(bool_lit) (nil_lit) (str_lit) (num_lit) (kwd_lit)] @anchor - (#set! "scope" "tail")) @align + ) @align ; Special indentation for def-like forms, let bindings, and other special forms ; These forms typically have the body indented by one level after the name/bindings @@ -50,11 +50,11 @@ ; Align vector/map elements when first two are on same line (e.g., let bindings) (vec_lit . (_) @anchor . (_) @second (#same-line? @anchor @second) - (#set! "scope" "tail")) @align + ) @align (map_lit . (_) @anchor . (_) @second (#same-line? @anchor @second) - (#set! "scope" "tail")) @align + ) @align ; Indent vectors, maps, and sets [(vec_lit) (map_lit) (set_lit)] @indent diff --git a/runtime/queries/cython/indents.scm b/runtime/queries/cython/indents.scm index 95243571..49f35211 100644 --- a/runtime/queries/cython/indents.scm +++ b/runtime/queries/cython/indents.scm @@ -41,7 +41,7 @@ "def") @indent @extend (ERROR (block) @indent @extend - (#set! "scope" "all")) + ) (ERROR "try" @@ -88,9 +88,9 @@ (parameters . (identifier) @anchor - (#set! "scope" "tail")) @align + ) @align (argument_list . (_) @anchor - (#set! "scope" "tail")) @align + ) @align diff --git a/runtime/queries/dart/indents.scm b/runtime/queries/dart/indents.scm index 386cb1e1..33f7183a 100644 --- a/runtime/queries/dart/indents.scm +++ b/runtime/queries/dart/indents.scm @@ -31,58 +31,58 @@ (for_statement body: _ @indent (#not-kind-eq? @indent block) - (#set! "scope" "all") + ) (while_statement body: _ @indent (#not-kind-eq? @indent block) - (#set! "scope" "all") + ) (do_statement body: _ @indent (#not-kind-eq? @indent block) - (#set! "scope" "all") + ) (if_statement consequence: _ @indent (#not-kind-eq? @indent block) - (#set! "scope" "all") + ) (if_statement alternative: _ @indent (#not-kind-eq? @indent if_statement) (#not-kind-eq? @indent block) - (#set! "scope" "all") + ) (if_statement "else" @else alternative: (if_statement) @indent (#not-same-line? @indent @else) - (#set! "scope" "all") + ) (if_element consequence: _ @indent - (#set! "scope" "all") + ) (if_element alternative: _ @indent (#not-kind-eq? @indent if_element) - (#set! "scope" "all") + ) (if_element "else" @else alternative: (if_element) @indent (#not-same-line? @indent @else) - (#set! "scope" "all") + ) (for_element body: _ @indent - (#set! "scope" "all") + ) ; simple statements @@ -102,5 +102,4 @@ ")" ] @outdent - (string_literal) @opaque diff --git a/runtime/queries/doxyfile/indents.scm b/runtime/queries/doxyfile/indents.scm index 5768fc6c..7c0d9514 100644 --- a/runtime/queries/doxyfile/indents.scm +++ b/runtime/queries/doxyfile/indents.scm @@ -1,3 +1,3 @@ (option value: (_) @anchor - (#set! "scope" "tail")) @align + ) @align diff --git a/runtime/queries/ecma/indents.scm b/runtime/queries/ecma/indents.scm index 558c9a57..19db0bb5 100644 --- a/runtime/queries/ecma/indents.scm +++ b/runtime/queries/ecma/indents.scm @@ -44,7 +44,7 @@ consequence: (_) @indent (#not-kind-eq? @indent "statement_block") alternative: (_) - (#set! "scope" "all")) + (#set! "scope" "header")) (else_clause (_) @_body (#not-kind-eq? @_body "statement_block") @@ -61,7 +61,7 @@ (do_statement body: (_) @indent (#not-kind-eq? @indent "statement_block") - (#set! "scope" "all")) + (#set! "scope" "header")) ; Template-literal bodies are literal content (between interpolations). (template_string) @opaque diff --git a/runtime/queries/janet/indents.scm b/runtime/queries/janet/indents.scm index b37c9bb7..e22f391f 100644 --- a/runtime/queries/janet/indents.scm +++ b/runtime/queries/janet/indents.scm @@ -3,7 +3,7 @@ ; (* 2) ; (print)) (par_tup_lit . (sym_lit) @first . (_) @anchor - (#set! "scope" "tail") + (#same-line? @first @anchor) ; anything that doesn't match should be indented normally ; from https://github.com/janet-lang/spork/blob/5601dc883535473bca28351cc6df04ed6c656c65/spork/fmt.janet#L87C12-L93C38 diff --git a/runtime/queries/java/indents.scm b/runtime/queries/java/indents.scm index 91dfc294..59caa939 100644 --- a/runtime/queries/java/indents.scm +++ b/runtime/queries/java/indents.scm @@ -28,21 +28,21 @@ (if_statement consequence: (_) @indent (#not-kind-eq? @indent "block") - (#set! "scope" "all")) + (#set! "scope" "header")) ; Braceless `else` body (the body is the alternative field). Skip `else if` ; (alternative is an if_statement) and braced bodies. (if_statement alternative: (_) @indent (#not-kind-eq? @indent "block") (#not-kind-eq? @indent "if_statement") - (#set! "scope" "all")) + (#set! "scope" "header")) (while_statement body: (_) @indent (#not-kind-eq? @indent "block") - (#set! "scope" "all")) + (#set! "scope" "header")) (for_statement body: (_) @indent (#not-kind-eq? @indent "block") - (#set! "scope" "all")) + (#set! "scope" "header")) (string_literal) @opaque diff --git a/runtime/queries/julia/indents.scm b/runtime/queries/julia/indents.scm index c6bcdafd..b8ec89e8 100644 --- a/runtime/queries/julia/indents.scm +++ b/runtime/queries/julia/indents.scm @@ -33,8 +33,8 @@ (argument_list . (_) @anchor - (#set! "scope" "tail")) @align + ) @align (curly_expression . (_) @anchor - (#set! "scope" "tail")) @align + ) @align diff --git a/runtime/queries/luau/indents.scm b/runtime/queries/luau/indents.scm index 06414742..10dc496e 100644 --- a/runtime/queries/luau/indents.scm +++ b/runtime/queries/luau/indents.scm @@ -30,7 +30,7 @@ [ (arglist) ] @indent - (#set! "scope" "all") + ) ( @@ -38,7 +38,7 @@ (explist) (interp_exp) ] @indent.always - (#set! "scope" "all") + ) [ @@ -58,7 +58,7 @@ . (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (field @@ -66,7 +66,7 @@ . field_indexer: (_) @indent (#not-same-line? @expr-start @indent) - (#set! "scope" "all") + ) (_ @@ -77,7 +77,7 @@ (_) @expr-end (#not-same-line? @indent @expr-start) (#same-line? @expr-end @indent) - (#set! "scope" "all") + ) (_ @@ -88,7 +88,7 @@ (_) @indent (#same-line? @expr-start @assign-sym) (#not-same-line? @assign-sym @indent) - (#set! "scope" "all") + ) (ifexp @@ -100,7 +100,7 @@ ] @expr-start . (_) @indent.always - (#set! "scope" "all") + (#not-same-line? @indent.always @expr-start) ) @@ -108,7 +108,7 @@ (paramtypelist) @expr-start return_type: (_) @indent (#not-same-line? @expr-start @indent) - (#set! "scope" "all") + ) (exp_wrap diff --git a/runtime/queries/nix/indents.scm b/runtime/queries/nix/indents.scm index 1534abaa..b050f137 100644 --- a/runtime/queries/nix/indents.scm +++ b/runtime/queries/nix/indents.scm @@ -40,11 +40,11 @@ ; branches, so the chain stays flat instead of stair-stepping. (if_expression consequence: (_) @indent - (#set! "scope" "all")) + (#set! "scope" "header")) (if_expression alternative: (_) @indent (#not-kind-eq? @indent "if_expression") - (#set! "scope" "all")) + (#set! "scope" "header")) ; Function application: arguments carried onto following lines. Nested ; applications share a line, so they collapse to a single level. diff --git a/runtime/queries/ohm/indents.scm b/runtime/queries/ohm/indents.scm index f56119ae..1b4e4441 100644 --- a/runtime/queries/ohm/indents.scm +++ b/runtime/queries/ohm/indents.scm @@ -27,7 +27,7 @@ ; | ... | ... (rule_body . (top_level_term) @anchor - (#set! "scope" "tail")) @align + ) @align ; N/A or unused: ; -------------- diff --git a/runtime/queries/ponylang/indents.scm b/runtime/queries/ponylang/indents.scm index ef9c8203..0d61e62a 100644 --- a/runtime/queries/ponylang/indents.scm +++ b/runtime/queries/ponylang/indents.scm @@ -13,7 +13,7 @@ ; ffi_call and call (_ arguments: (_) @indent) (assignment right: (_) @indent - (#set! "scope" "all") + ) [ diff --git a/runtime/queries/python/indents.scm b/runtime/queries/python/indents.scm index 69e1e00e..a22a06ae 100644 --- a/runtime/queries/python/indents.scm +++ b/runtime/queries/python/indents.scm @@ -40,7 +40,7 @@ "def") @indent @extend (ERROR (block) @indent @extend - (#set! "scope" "all")) + ) [ (if_statement) @@ -83,12 +83,11 @@ (parameters . (identifier) @anchor - (#set! "scope" "tail")) @align + ) @align (argument_list . (_) @anchor - (#set! "scope" "tail")) @align - + ) @align ; String bodies (triple-quoted strings span lines) are literal content. (string) @opaque diff --git a/runtime/queries/rust/indents.scm b/runtime/queries/rust/indents.scm index bb3ee308..1c245111 100644 --- a/runtime/queries/rust/indents.scm +++ b/runtime/queries/rust/indents.scm @@ -40,49 +40,49 @@ (_) @expr-start right: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (compound_assignment_expr . (_) @expr-start right: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (let_declaration "let" @expr-start value: (_) @indent alternative: (_)? @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (let_condition . (_) @expr-start value: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (if_expression . (_) @expr-start condition: (_) @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") + ) (field_pattern . (_) @expr-start pattern: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) ; Indent type aliases that span multiple lines, similar to ; regular assignment expressions @@ -91,7 +91,7 @@ (_) @expr-start type: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) ; Some field expressions where the left part is a multiline expression are not @@ -122,7 +122,6 @@ (#match? @val "(\\A[^\\n\\r]+(\\(|\\{|\\[)[\\t ]*(\\n|\\r))") ) - ; Indent if guards in patterns. ; Since the tree-sitter grammar doesn't create a node for the if expression, ; it's not possible to do this correctly in all cases. Indenting the tail of the @@ -150,9 +149,8 @@ . (_) @indent (#not-same-line? @in @indent) - (#set! "scope" "all") + ) - ; Multi-line string / raw-string bodies are literal content: preserve them. [ diff --git a/runtime/queries/scheme/indents.scm b/runtime/queries/scheme/indents.scm index b00b2965..94caf4c8 100644 --- a/runtime/queries/scheme/indents.scm +++ b/runtime/queries/scheme/indents.scm @@ -5,16 +5,16 @@ ; If a list has 2 elements on the first line, it is aligned to the second element. (list . (_) @first . (_) @anchor (#same-line? @first @anchor) - (#set! "scope" "tail") + (#not-kind-eq? @first "boolean") (#not-kind-eq? @first "character") (#not-kind-eq? @first "string") (#not-kind-eq? @first "number") (#not-match? @first "def.*|let.*|set!")) @align ; If the first element in a list is also a list and on a line by itself, the outer list is aligned to it (list . (list) @anchor . - (#set! "scope" "tail") + (#not-kind-eq? @first "boolean") (#not-kind-eq? @first "character") (#not-kind-eq? @first "string") (#not-kind-eq? @first "number")) @align (list . (list) @anchor . (_) @second (#not-same-line? @anchor @second) - (#set! "scope" "tail") + (#not-kind-eq? @first "boolean") (#not-kind-eq? @first "character") (#not-kind-eq? @first "string") (#not-kind-eq? @first "number") (#not-match? @first "def.*|let.*|set!")) @align ; If the first element in a list is not a list and on a line by itself, the outer list is aligned to @@ -32,7 +32,7 @@ ; If the first element in a list is a literal, align the list to it (list . [(boolean) (character) (string) (number)] @anchor - (#set! "scope" "tail")) @align + ) @align ; If the first element is among a set of predefined keywords, align the list to this element ; plus 1 space (using the same workaround as above for now). This is a simplification since actually diff --git a/runtime/queries/sway/indents.scm b/runtime/queries/sway/indents.scm index e6902b62..5d6ccf0a 100644 --- a/runtime/queries/sway/indents.scm +++ b/runtime/queries/sway/indents.scm @@ -35,14 +35,14 @@ (_) @expr-start right: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (compound_assignment_expr . (_) @expr-start right: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (let_declaration . @@ -50,14 +50,14 @@ value: (_) @indent alternative: (_)? @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (if_expression . (_) @expr-start condition: (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) ; Some field expressions where the left part is a multiline expression are not diff --git a/runtime/queries/swift/indents.scm b/runtime/queries/swift/indents.scm index afd7b42d..c9c310d7 100644 --- a/runtime/queries/swift/indents.scm +++ b/runtime/queries/swift/indents.scm @@ -43,7 +43,7 @@ (_) @expr-start (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (control_transfer_statement @@ -51,7 +51,7 @@ (_) @expr-start (_) @indent (#not-same-line? @indent @expr-start) - (#set! "scope" "all") + ) (if_statement