Skip to main content

lattice_syntax/
indent.rs

1//! Where should this line start?
2//!
3//! The indent engine, beside its peers: [`crate::text_objects`] and
4//! [`crate::motions`] are the same shape -- computation over the parse
5//! tree, driven by `.scm` files this crate already owns. IN.2 adds the
6//! `indents.scm` query evaluator here; IN.1 ships the **lexical**
7//! half below, which is what runs when no tree is available.
8//!
9//! Pure and synchronous by construction -- no I/O, no async, no host
10//! state -- because every consumer sits on the keystroke path
11//! (`docs/dev/architecture/auto-indent.md` §2).
12//!
13//! The [`IndentUnit`] value itself lives in `lattice-core`, not here:
14//! the `>` / `<` operators in `lattice-grammar` consume it, and this
15//! crate depends on `lattice-grammar`, so owning it here would be a
16//! cycle.
17//!
18//! Two policies over one mechanism
19//! ------------------------------
20//!
21//! - [`IndentMethod::Keep`] -- copy the previous non-blank line's
22//!   indent. Vim's `autoindent`. No scan, no cleverness.
23//! - [`IndentMethod::Syntax`]'s **fallback** -- the copy, plus one level
24//!   if the previous line leaves a bracket unclosed, minus one if the
25//!   target line opens with a closer. Vim's `smartindent`, roughly.
26//!
27//! Vim keeps these separate for a reason worth preserving: the bracket
28//! rule misfires in a language where `{` is not a block opener, and
29//! `keep` is what a user picks when they want the dumbest predictable
30//! thing. Rather than special-casing that, the bracket sets are
31//! **per-language** and languages with no bracket notion (plain text,
32//! markdown) have empty sets -- at which point the bridge degrades to
33//! `keep` on its own.
34//!
35//! What this deliberately does not do
36//! ----------------------------------
37//!
38//! The scan is **lexical**, so it cannot tell a brace in code from a
39//! brace in a string or a comment. `println!("{")` counts as an opener
40//! here. That is a known and accepted wrong answer: the whole point of
41//! this half is to be the thing that still works when no parse tree is
42//! available, and a scan sophisticated enough to track string state
43//! per-language would be a worse, slower duplicate of what IN.2's
44//! tree-sitter path does properly. When the tree is available,
45//! `syntax` uses it and never reaches here.
46
47use lattice_core::{IndentMethod, IndentUnit};
48
49use crate::Lang;
50use crate::syntax::SyntaxSnapshot;
51
52// ──────────────────────────────────────────────────────────────
53// IN.2 — the tree-sitter half
54// ──────────────────────────────────────────────────────────────
55
56// Embedded `indents.scm` sources. Files live at
57// `crates/lattice-syntax/queries/<lang>/indents.scm`, beside their
58// `folds` / `symbols` / `textobjects` siblings; shipped in the binary
59// via `include_str!` with no runtime path lookup.
60const RUST_INDENTS_QUERY: &str = include_str!("../queries/rust/indents.scm");
61// IN.3 — the brace family. One query shape, node names swapped.
62const C_INDENTS_QUERY: &str = include_str!("../queries/c/indents.scm");
63const CPP_INDENTS_QUERY: &str = include_str!("../queries/cpp/indents.scm");
64const CSS_INDENTS_QUERY: &str = include_str!("../queries/css/indents.scm");
65const GO_INDENTS_QUERY: &str = include_str!("../queries/go/indents.scm");
66const JAVA_INDENTS_QUERY: &str = include_str!("../queries/java/indents.scm");
67const JAVASCRIPT_INDENTS_QUERY: &str = include_str!("../queries/javascript/indents.scm");
68const JSON_INDENTS_QUERY: &str = include_str!("../queries/json/indents.scm");
69const TSX_INDENTS_QUERY: &str = include_str!("../queries/tsx/indents.scm");
70const TYPESCRIPT_INDENTS_QUERY: &str = include_str!("../queries/typescript/indents.scm");
71// IN.4 — indent-sensitive + scripting. These close with WORDS (`end`,
72// `fi`, `done`, `esac`) as often as with punctuation, and two of them
73// carry indentation as data inside heredocs / docstrings.
74const BASH_INDENTS_QUERY: &str = include_str!("../queries/bash/indents.scm");
75const LUA_INDENTS_QUERY: &str = include_str!("../queries/lua/indents.scm");
76const PYTHON_INDENTS_QUERY: &str = include_str!("../queries/python/indents.scm");
77const RUBY_INDENTS_QUERY: &str = include_str!("../queries/ruby/indents.scm");
78// IN.5 — data + markup. `sql` and `markdown` deliberately ship NO
79// query; see `indents_source` for why.
80const HTML_INDENTS_QUERY: &str = include_str!("../queries/html/indents.scm");
81const TOML_INDENTS_QUERY: &str = include_str!("../queries/toml/indents.scm");
82const YAML_INDENTS_QUERY: &str = include_str!("../queries/yaml/indents.scm");
83
84/// The `indents.scm` source for a registry language name, or `None`
85/// when that language does not ship one yet.
86///
87/// Keyed by the registry's language *name* rather than [`Lang`] because
88/// the registry compiles queries per registered config (including
89/// `markdown_inline`, which has no `Lang` variant of its own).
90///
91/// A language absent from this table is not broken -- predictive indent
92/// falls back to the lexical bridge for it, which is vim's
93/// `smartindent`. IN.3–IN.5 fill the table in.
94pub(crate) fn indents_source(name: &str) -> Option<&'static str> {
95    match name {
96        "rust" => Some(RUST_INDENTS_QUERY),
97        // IN.3 — the brace family.
98        "c" => Some(C_INDENTS_QUERY),
99        "cpp" => Some(CPP_INDENTS_QUERY),
100        "css" => Some(CSS_INDENTS_QUERY),
101        "go" => Some(GO_INDENTS_QUERY),
102        "java" => Some(JAVA_INDENTS_QUERY),
103        "javascript" => Some(JAVASCRIPT_INDENTS_QUERY),
104        "json" => Some(JSON_INDENTS_QUERY),
105        "tsx" => Some(TSX_INDENTS_QUERY),
106        "typescript" => Some(TYPESCRIPT_INDENTS_QUERY),
107        // IN.4 — indent-sensitive + scripting.
108        "bash" => Some(BASH_INDENTS_QUERY),
109        "lua" => Some(LUA_INDENTS_QUERY),
110        "python" => Some(PYTHON_INDENTS_QUERY),
111        "ruby" => Some(RUBY_INDENTS_QUERY),
112        // IN.5 — data + markup.
113        "html" => Some(HTML_INDENTS_QUERY),
114        "toml" => Some(TOML_INDENTS_QUERY),
115        "yaml" => Some(YAML_INDENTS_QUERY),
116        // `markdown` and `sql` ship NO query, deliberately:
117        //
118        // - **markdown** nests by CONTENT WIDTH, not by a fixed unit.
119        //   A nested list item aligns under its parent's text, which
120        //   depends on the marker (`-` vs `10.`), so a fixed
121        //   `shiftwidth` step is the wrong model and would fight the
122        //   user. The lexical bridge's copy-the-previous-line is
123        //   closer to right, and markdown's `BracketSyntax::NONE`
124        //   already stops a stray brace from indenting prose.
125        // - **sql** is parsed by `tree-sitter-sequel`, a deliberately
126        //   permissive multi-dialect grammar, and SQL indentation
127        //   convention varies more between houses than between
128        //   dialects (leading vs trailing commas, `AND` alignment,
129        //   river style). There is no default worth imposing; `=`
130        //   plus an external rung on `format.indent` is the honest answer.
131        //
132        // Both degrade to the lexical bridge, which is the cascade
133        // working as designed rather than a gap.
134        _ => None,
135    }
136}
137
138/// What an `indents.scm` capture asks for.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140enum Capture {
141    /// `@indent` — children sit one level deeper. Collapses with
142    /// another `@indent` starting on the same row, so
143    /// `foo(bar(` opens one level, not two.
144    Indent,
145    /// `@indent.always` — as `Indent`, without the same-row collapse.
146    IndentAlways,
147    /// `@outdent` — a line starting with this node sits one level
148    /// shallower, cancelling the enclosing `@indent`.
149    Outdent,
150    /// `@outdent.always` — as `Outdent`, without collapsing.
151    OutdentAlways,
152}
153
154impl Capture {
155    fn parse(name: &str) -> Option<Self> {
156        match name {
157            "indent" => Some(Self::Indent),
158            "indent.always" => Some(Self::IndentAlways),
159            "outdent" => Some(Self::Outdent),
160            "outdent.always" => Some(Self::OutdentAlways),
161            // `@extend` / `@extend.prevent-once` / `@align` are
162            // recognised by the dialect but not evaluated in v1 (design
163            // §4.1). Unknown captures are ignored rather than rejected,
164            // so a query written against the fuller vocabulary still
165            // loads and simply contributes less.
166            _ => None,
167        }
168    }
169}
170
171/// Every captured node in the queried range, keyed by node id.
172type CaptureMap = std::collections::HashMap<usize, Capture>;
173
174/// Run the language's `indents.scm` over `scope` and index the results
175/// by node id.
176///
177/// **Scoped to one top-level item, and that is a hard perf
178/// requirement rather than an optimisation.** An earlier revision ran
179/// the query over the whole file and benched at 623 µs on an 800-line
180/// buffer and 2.57 ms on a 3200-line one — linear in file size, on the
181/// keystroke path, which would put `dispatch.rs` at ~30 ms per `<CR>`
182/// and blow the frame by 4×.
183///
184/// Scoping is sound, not a trade: the only nodes ever consulted are
185/// ancestors of the query position, and every ancestor except the root
186/// is contained in the root's child that contains the position. The
187/// root itself is never captured. So a whole-file run computes
188/// thousands of captures to look up a handful.
189fn capture_map(snapshot: &SyntaxSnapshot, scope: tree_sitter::Node<'_>) -> Option<CaptureMap> {
190    use tree_sitter::{QueryCursor, StreamingIterator};
191
192    let query = snapshot.registry().indents_query(snapshot.lang().name())?;
193    let source = snapshot.source();
194
195    let mut cursor = QueryCursor::new();
196    cursor.set_byte_range(scope.start_byte()..scope.end_byte());
197    let mut matches = cursor.matches(query, scope, source);
198
199    let names = query.capture_names();
200    let mut map = CaptureMap::new();
201    while let Some(m) = matches.next() {
202        for cap in m.captures {
203            let Some(name) = names.get(cap.index as usize) else {
204                continue;
205            };
206            let Some(kind) = Capture::parse(name) else {
207                continue;
208            };
209            map.insert(cap.node.id(), kind);
210        }
211    }
212    Some(map)
213}
214
215/// The top-level item that governs the indent at `at`: the root child
216/// containing it, or -- when `at` sits between items -- the one
217/// immediately before it.
218///
219/// Serves two purposes at once, which is why it is one function:
220///
221/// 1. It bounds [`capture_map`] to a single item instead of the file.
222/// 2. It is where the parse error that matters would be. Tree-sitter
223///    does not represent incomplete code as "a block that happens to
224///    be unclosed" -- it collapses the region into an `ERROR` node with
225///    no structure inside. Parsing `fn f() {\n    x();\n\n` yields
226///    exactly one `ERROR [0..17]` and **no `block` node at all**, so
227///    `@indent` matches nothing and a naive engine answers zero for a
228///    cursor plainly inside a function body.
229///
230/// The "immediately before" case is what catches that: the cursor sits
231/// at byte 18, past the ERROR's end, so no *containing* item exists and
232/// only the preceding sibling reveals the broken parse.
233///
234/// Returns `None` when the governing item is errored -- the tree has no
235/// answer, and the caller falls back to the lexical bridge, which
236/// handles unclosed openers correctly by construction because a bracket
237/// scan does not need the block to be finished. Scoping the check this
238/// way also means a syntax error elsewhere in the file does not disable
239/// indentation here.
240fn governing_scope(root: tree_sitter::Node<'_>, at: usize) -> Option<tree_sitter::Node<'_>> {
241    // Binary search, not a scan: root children are in byte order, and a
242    // linear walk makes this O(top-level items). Measured 29.7 µs at
243    // 3200 lines against 8.4 µs at 80 -- still under budget, but growing,
244    // and a 36k-line file is one people actually open. Searching flattens
245    // it to 9.5 µs.
246    //
247    // This is the same bug TS.1 hit and fixed (benchmarks.md): a linear
248    // `0..child_count` walk at the root of a large file, measured there
249    // at 175 µs. It is an easy shape to write and invisible without a
250    // size sweep, which is why both benches keep their row.
251    let count = root.child_count() as u32;
252    let (mut lo, mut hi) = (0u32, count);
253    let mut best: Option<tree_sitter::Node<'_>> = None;
254    while lo < hi {
255        let mid = lo + (hi - lo) / 2;
256        let Some(child) = root.child(mid) else { break };
257        if child.start_byte() <= at {
258            best = Some(child);
259            lo = mid + 1;
260        } else {
261            hi = mid;
262        }
263    }
264    // `best` is the last child starting at or before `at` — the one
265    // containing it, or the one immediately before when `at` sits in
266    // the gap between items.
267    let scope = best.unwrap_or(root);
268    if scope.has_error() || scope.is_error() || scope.is_missing() {
269        return None;
270    }
271    Some(scope)
272}
273
274/// The chain of nodes containing `at`, deepest first.
275fn ancestor_chain(snapshot: &SyntaxSnapshot, at: usize) -> Option<Vec<tree_sitter::Node<'_>>> {
276    let tree = snapshot.tree()?;
277    let at = at.min(snapshot.source().len());
278    let mut node = tree.root_node().descendant_for_byte_range(at, at)?;
279    let mut chain = vec![node];
280    while let Some(parent) = node.parent() {
281        chain.push(parent);
282        node = parent;
283    }
284    Some(chain)
285}
286
287/// Indent level, in steps, for a **new** line inserted at byte `at`.
288///
289/// Counts the `@indent` nodes that are genuinely *open* at `at`:
290/// `start_byte < at < end_byte`. Both bounds are strict, and the upper
291/// one matters — with `at <= end_byte`, a cursor sitting just past a
292/// block's closing brace (`fn f() {}|`) would still count the block and
293/// indent the next line by one.
294///
295/// Returns `None` when there is no tree or no query for the language,
296/// which is the caller's signal to use the lexical bridge.
297pub fn tree_levels_for_new_line(snapshot: &SyntaxSnapshot, at: usize) -> Option<i32> {
298    let at = at.min(snapshot.source().len());
299    // IN.4: inside a string, indentation is CONTENT. A Python
300    // docstring, a Bash heredoc, a Rust raw string or a JS template
301    // literal all carry their leading whitespace as data, so applying
302    // the enclosing block's structural indent would silently edit the
303    // string's value -- a correctness bug, not a cosmetic one.
304    //
305    // Declining hands off to the lexical bridge, which copies the
306    // previous line's indent. That is also what vim does inside a
307    // string, so the behaviour is both safer and unsurprising.
308    if snapshot.cursor_in_string_scope(at) {
309        return None;
310    }
311    let scope = governing_scope(snapshot.tree()?.root_node(), at)?;
312    let chain = ancestor_chain(snapshot, at)?;
313    let map = capture_map(snapshot, scope)?;
314
315    let mut level = 0i32;
316    let mut counted_rows = std::collections::HashSet::new();
317    for node in chain {
318        let Some(kind) = map.get(&node.id()) else {
319            continue;
320        };
321        let open = node.start_byte() < at && at < node.end_byte();
322        match kind {
323            Capture::Indent if open => {
324                // Collapse siblings opening on the same row: `foo(bar(`
325                // is one level, not two.
326                if counted_rows.insert(node.start_position().row) {
327                    level += 1;
328                }
329            }
330            Capture::IndentAlways if open => level += 1,
331            // Outdent captures describe a line that STARTS with the
332            // node; a newly created empty line starts with nothing, so
333            // they contribute nothing here. The moved tail's leading
334            // closer is handled by the caller.
335            _ => {}
336        }
337    }
338    Some(level.max(0))
339}
340
341/// Indent level, in steps, for the **existing** line `row`.
342///
343/// Used by `=` and electric reindent (IN.6 / IN.7). Row-based rather
344/// than byte-based because the question is different: a block that
345/// *starts* on this row must not indent it, and a closer that starts on
346/// this row must dedent it.
347pub fn tree_levels_for_line(snapshot: &SyntaxSnapshot, row: u32) -> Option<i32> {
348    let source = snapshot.source();
349    let (line_start, line_end) = line_bounds(source, row)?;
350    // Anchor on the line's first non-whitespace byte: that is the token
351    // whose alignment is being decided, and it is what `@outdent` has
352    // to match against.
353    let at = (line_start..line_end)
354        .find(|i| !matches!(source.get(*i), Some(b' ') | Some(b'\t')))
355        .unwrap_or(line_start);
356
357    let scope = governing_scope(snapshot.tree()?.root_node(), at)?;
358    let chain = ancestor_chain(snapshot, at)?;
359    let map = capture_map(snapshot, scope)?;
360
361    let mut level = 0i32;
362    let mut counted_rows = std::collections::HashSet::new();
363    for node in chain {
364        let Some(kind) = map.get(&node.id()) else {
365            continue;
366        };
367        let start_row = node.start_position().row as u32;
368        let end_row = node.end_position().row as u32;
369        match kind {
370            Capture::Indent if start_row < row && row <= end_row => {
371                if counted_rows.insert(start_row) {
372                    level += 1;
373                }
374            }
375            Capture::IndentAlways if start_row < row && row <= end_row => level += 1,
376            Capture::Outdent | Capture::OutdentAlways if start_row == row => level -= 1,
377            _ => {}
378        }
379    }
380    Some(level.max(0))
381}
382
383/// Byte bounds of `row`, excluding its newline.
384fn line_bounds(source: &[u8], row: u32) -> Option<(usize, usize)> {
385    let mut start = 0usize;
386    let mut seen = 0u32;
387    for (i, b) in source.iter().enumerate() {
388        if seen == row {
389            break;
390        }
391        if *b == b'\n' {
392            seen += 1;
393            start = i + 1;
394        }
395    }
396    if seen < row {
397        return None;
398    }
399    let end = source[start..]
400        .iter()
401        .position(|b| *b == b'\n')
402        .map(|o| start + o)
403        .unwrap_or(source.len());
404    Some((start, end))
405}
406
407/// Per-language bracket sets for the opener/closer scan.
408///
409/// Empty sets are meaningful, not a null case: they are how a language
410/// with no bracket-block notion opts out of the scan and gets pure
411/// `keep` behaviour from the same code path.
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
413pub struct BracketSyntax {
414    pub openers: &'static [u8],
415    pub closers: &'static [u8],
416}
417
418impl BracketSyntax {
419    /// `{`, `(`, `[` -- the C-family set, correct for every bundled
420    /// language whose blocks are brace-delimited.
421    pub const BRACES: Self = Self {
422        openers: b"{([",
423        closers: b"})]",
424    };
425
426    /// No brackets: the scan is skipped entirely and the bridge
427    /// behaves as `keep`.
428    pub const NONE: Self = Self {
429        openers: b"",
430        closers: b"",
431    };
432
433    /// The bracket sets for a language.
434    ///
435    /// Prose languages get [`Self::NONE`] so a stray brace in a
436    /// sentence does not indent the next line. Everything else gets
437    /// [`Self::BRACES`] -- including the indent-sensitive languages
438    /// (Python, YAML), where brackets still bound continuation lines
439    /// even though blocks are not brace-delimited, which is exactly
440    /// the case the scan gets right.
441    pub fn for_lang(lang: Lang) -> Self {
442        match lang {
443            Lang::Plain | Lang::Markdown => Self::NONE,
444            _ => Self::BRACES,
445        }
446    }
447
448    fn is_empty(&self) -> bool {
449        self.openers.is_empty() && self.closers.is_empty()
450    }
451
452    /// Net bracket depth `line` leaves open. Negative when the line
453    /// closes more than it opens.
454    fn net_depth(&self, line: &str) -> i32 {
455        let mut depth = 0i32;
456        for b in line.bytes() {
457            if self.openers.contains(&b) {
458                depth += 1;
459            } else if self.closers.contains(&b) {
460                depth -= 1;
461            }
462        }
463        depth
464    }
465
466    /// Whether `line`'s first non-whitespace byte is a closer.
467    ///
468    /// Public because the tree path needs it too: at query time the
469    /// closer about to move down with the cursor is still on the line
470    /// above, so the tree cannot see it and the caller applies the
471    /// dedent itself.
472    pub fn starts_with_closer(&self, line: &str) -> bool {
473        line.bytes()
474            .find(|b| *b != b' ' && *b != b'\t')
475            .is_some_and(|b| self.closers.contains(&b))
476    }
477}
478
479/// The whitespace a newly created line should start with.
480///
481/// `prev` is the text that ends up ABOVE the new line, which differs
482/// by the key that created it:
483///
484/// - `o` / `O` -- the nearest non-blank line. (Vim takes `O`'s indent
485///   from the line it pushes down, which is the line the cursor is on,
486///   so both use the same source.)
487/// - `<CR>` -- the **head**, i.e. the text before the cursor, not the
488///   whole line. In `foo(a, |b)` the whole line is bracket-balanced
489///   while the head leaves `(` open; only the head gives the right
490///   answer.
491///
492/// `None` means there is nothing above, i.e. the top of the buffer.
493///
494/// `next` is the text that will follow on the new line, used only for
495/// the closer check. Pass `None` for the ordinary
496/// create-an-empty-line case; pass the tail for `<CR>` pressed
497/// mid-line, where the text after the cursor moves down with it and a
498/// leading `}` should dedent.
499///
500/// Returns the whitespace string, not a column count, because the
501/// caller splices it into an edit and the rendering (tabs vs spaces)
502/// is the unit's business.
503pub fn indent_for_new_line(
504    method: IndentMethod,
505    prev: Option<&str>,
506    next: Option<&str>,
507    unit: IndentUnit,
508    brackets: BracketSyntax,
509) -> String {
510    let columns = indent_columns_for_new_line(method, prev, next, unit, brackets);
511    unit.render(columns)
512}
513
514/// [`indent_for_new_line`] in display columns, before rendering.
515/// Separate so IN.2's engine can compare its own answer against the
516/// fallback's without allocating.
517pub fn indent_columns_for_new_line(
518    method: IndentMethod,
519    prev: Option<&str>,
520    next: Option<&str>,
521    unit: IndentUnit,
522    brackets: BracketSyntax,
523) -> u16 {
524    if matches!(method, IndentMethod::None) {
525        return 0;
526    }
527    let Some(prev) = prev else { return 0 };
528    let base = unit.columns_of(prev);
529
530    // `Keep` is a pure copy -- see the module doc. `Syntax` reaching
531    // here means the tree was unavailable, and the bracket scan is the
532    // best guess left.
533    if matches!(method, IndentMethod::Keep) || brackets.is_empty() {
534        return base;
535    }
536
537    let mut columns = base;
538    if brackets.net_depth(prev) > 0 {
539        columns = unit.shift(columns, 1);
540    }
541    if next.is_some_and(|n| brackets.starts_with_closer(n)) {
542        columns = unit.shift(columns, -1);
543    }
544    columns
545}
546
547// ──────────────────────────────────────────────────────────────
548// IN.6 — electric reindent
549// ──────────────────────────────────────────────────────────────
550
551/// Words that, when they *begin* a line, put it one level shallower.
552///
553/// The punctuation closers (`}`, `)`, `]`) are already handled by
554/// [`BracketSyntax::starts_with_closer`]; this covers the languages
555/// that close with words instead, plus the continuation keywords
556/// (`else`, `elif`) that sit back at the construct's level while the
557/// body carries on.
558fn dedent_keywords(lang: Lang) -> &'static [&'static str] {
559    match lang {
560        Lang::Lua => &["end", "until", "else", "elseif"],
561        Lang::Ruby => &["end", "else", "elsif", "when", "rescue", "ensure"],
562        Lang::Bash => &["fi", "done", "esac", "else", "elif"],
563        // Python's suite has no closer, but these continuation
564        // keywords do step back out of the preceding block.
565        Lang::Python => &["else", "elif", "except", "finally"],
566        _ => &[],
567    }
568}
569
570/// The first whitespace-delimited word of `line`, if any.
571fn leading_word(line: &str) -> &str {
572    let t = line.trim_start();
573    let end = t
574        .find(|c: char| !c.is_alphanumeric() && c != '_')
575        .unwrap_or(t.len());
576    &t[..end]
577}
578
579/// Whether typing `typed` at the end of `line_head` should re-indent
580/// the current line.
581///
582/// Two triggers, matching the two ways a language closes a block:
583///
584/// - `typed` is a bracket closer and nothing but whitespace precedes
585///   it on the line. The "nothing but whitespace" part matters — a `}`
586///   at the end of `let x = Foo { a };` must not re-indent the line.
587/// - `line_head + typed` completes a dedent keyword that begins the
588///   line. Checked as a whole word so `end` fires but `append` and
589///   `defenders` do not.
590pub fn is_electric_trigger(lang: Lang, line_head: &str, typed: char) -> bool {
591    let brackets = BracketSyntax::for_lang(lang);
592    let mut buf = [0u8; 4];
593    let typed_str = typed.encode_utf8(&mut buf);
594
595    if brackets.closers.contains(&(typed as u32 as u8)) && line_head.trim().is_empty() {
596        return true;
597    }
598
599    let words = dedent_keywords(lang);
600    if words.is_empty() {
601        return false;
602    }
603    // The keyword must be the whole of the line's content so far --
604    // otherwise `x = end` or a trailing `else` in a comment would fire.
605    let candidate = format!("{}{}", line_head.trim_start(), typed_str);
606    words.contains(&candidate.as_str())
607        && line_head.trim_start() == &candidate[..candidate.len() - typed_str.len()]
608}
609
610/// The indent, in columns, for a line being electrically re-indented.
611///
612/// **Deliberately lexical, not tree-driven**, and that follows from
613/// IN.2's finding rather than being a shortcut. At the instant the
614/// closer is typed, two things are true at once: the published snapshot
615/// has not caught up with the edit, and the code around the cursor is
616/// half-written — so a *fresh* parse would produce an `ERROR` node with
617/// no block structure and the engine would decline anyway. There is no
618/// version of this that the tree can answer, so asking it would cost a
619/// parse to learn nothing.
620///
621/// It reuses [`indent_columns_for_new_line`] with the current line
622/// passed as `next`: "the indent a new line here would get, given what
623/// this line starts with" is exactly the question, so no second
624/// algorithm is needed. Word closers subtract the extra level the
625/// bracket scan cannot see.
626pub fn electric_columns(
627    lang: Lang,
628    prev_nonblank: Option<&str>,
629    line: &str,
630    unit: IndentUnit,
631) -> u16 {
632    let brackets = BracketSyntax::for_lang(lang);
633    let mut columns = indent_columns_for_new_line(
634        IndentMethod::Syntax,
635        prev_nonblank,
636        Some(line),
637        unit,
638        brackets,
639    );
640    if !brackets.starts_with_closer(line) && dedent_keywords(lang).contains(&leading_word(line)) {
641        columns = unit.shift(columns, -1);
642    }
643    columns
644}
645
646#[cfg(test)]
647mod electric_tests {
648    use super::*;
649
650    fn unit() -> IndentUnit {
651        IndentUnit::new(4, true, 4)
652    }
653
654    #[test]
655    fn a_closer_typed_alone_on_a_line_triggers() {
656        assert!(is_electric_trigger(Lang::Rust, "        ", '}'));
657        assert!(is_electric_trigger(Lang::Rust, "", ')'));
658    }
659
660    #[test]
661    fn a_closer_after_content_does_not_trigger() {
662        // `let x = Foo { a };` — the `}` closes an inline literal and
663        // must not re-indent the line the user is writing.
664        assert!(!is_electric_trigger(
665            Lang::Rust,
666            "    let x = Foo { a ",
667            '}'
668        ));
669    }
670
671    #[test]
672    fn word_closers_trigger_only_as_whole_words() {
673        assert!(is_electric_trigger(Lang::Lua, "  en", 'd'));
674        assert!(is_electric_trigger(Lang::Ruby, "  en", 'd'));
675        assert!(is_electric_trigger(Lang::Bash, "  f", 'i'));
676        // `append` must not fire on its final `d`... it does not even
677        // end in one, so use a real near-miss: `bend`.
678        assert!(!is_electric_trigger(Lang::Lua, "  ben", 'd'));
679        // Nor mid-expression.
680        assert!(!is_electric_trigger(Lang::Lua, "  x = en", 'd'));
681    }
682
683    #[test]
684    fn languages_without_word_closers_only_trigger_on_brackets() {
685        assert!(!is_electric_trigger(Lang::Rust, "  en", 'd'));
686        assert!(is_electric_trigger(Lang::Rust, "  ", '}'));
687    }
688
689    #[test]
690    fn a_closing_brace_lands_at_the_openers_level() {
691        // The canonical case: `}` typed under an over-indented body
692        // snaps back to the `if`'s level.
693        let cols = electric_columns(Lang::Rust, Some("        y();"), "        }", unit());
694        assert_eq!(cols, 4);
695    }
696
697    #[test]
698    fn a_closer_directly_under_its_opener_lands_at_the_openers_level() {
699        let cols = electric_columns(Lang::Rust, Some("fn f() {"), "}", unit());
700        assert_eq!(cols, 0);
701    }
702
703    #[test]
704    fn word_closers_dedent_from_the_body() {
705        let cols = electric_columns(Lang::Lua, Some("    y()"), "    end", unit());
706        assert_eq!(cols, 0);
707        let cols = electric_columns(Lang::Bash, Some("    y"), "    fi", unit());
708        assert_eq!(cols, 0);
709    }
710
711    #[test]
712    fn a_word_closer_is_not_double_counted_with_a_bracket() {
713        // `}` is a bracket closer AND some languages have word
714        // closers; a line starting with `}` must lose exactly one
715        // level, not two.
716        let cols = electric_columns(Lang::Ruby, Some("    y"), "    }", unit());
717        assert_eq!(cols, 0);
718    }
719}
720
721#[cfg(test)]
722mod tree_tests {
723    use super::*;
724    use crate::syntax::Syntax;
725
726    /// Parse `src` as Rust and return the owned snapshot the engine
727    /// reads.
728    fn rust(src: &str) -> crate::syntax::SyntaxSnapshot {
729        let mut s = Syntax::for_language(Lang::Rust)
730            .expect("rust registered")
731            .expect("rust has a grammar");
732        s.parse(src);
733        s.snapshot_owned()
734    }
735
736    /// Level for a new line inserted at the byte marked `|` in `src`.
737    fn levels_after_cursor(src_with_cursor: &str) -> Option<i32> {
738        let at = src_with_cursor.find('|').expect("mark the cursor with |");
739        let src = src_with_cursor.replace('|', "");
740        tree_levels_for_new_line(&rust(src.as_str()), at)
741    }
742
743    #[test]
744    fn a_language_without_a_query_yields_none() {
745        // The signal the caller uses to fall back to the lexical
746        // bridge. The contract under test is "a language with no query
747        // degrades instead of failing", so this points at whatever is
748        // currently uncovered -- Python at IN.2, TOML at IN.4, SQL now.
749        // SQL is the stable home: it ships no query by DECISION rather
750        // than by not-yet (see `indents_source`), so this should not
751        // need moving again.
752        let mut s = Syntax::for_language(Lang::Sql)
753            .expect("sql registered")
754            .expect("sql has a grammar");
755        s.parse("select a\nfrom t;\n");
756        assert_eq!(tree_levels_for_new_line(&s.snapshot_owned(), 9), None);
757    }
758
759    #[test]
760    fn an_unparsed_snapshot_yields_none() {
761        let s = Syntax::for_language(Lang::Rust).unwrap().unwrap();
762        assert_eq!(tree_levels_for_new_line(&s.snapshot_owned(), 0), None);
763    }
764
765    #[test]
766    fn inside_a_block_is_one_level() {
767        assert_eq!(levels_after_cursor("fn f() {|}"), Some(1));
768        assert_eq!(levels_after_cursor("fn f() {\n    x();|\n}\n"), Some(1));
769    }
770
771    #[test]
772    fn past_the_closing_brace_is_zero() {
773        // The strict upper bound. With `at <= end_byte` this returns
774        // 1 and `<CR>` after a finished function indents for no
775        // reason.
776        assert_eq!(levels_after_cursor("fn f() {}|"), Some(0));
777        assert_eq!(levels_after_cursor("fn f() {\n    x();\n}|\n"), Some(0));
778    }
779
780    #[test]
781    fn before_the_opening_brace_is_zero() {
782        assert_eq!(levels_after_cursor("fn f() |{}"), Some(0));
783    }
784
785    #[test]
786    fn nesting_accumulates() {
787        let src = "fn f() {\n    if x {\n        y();|\n    }\n}\n";
788        assert_eq!(levels_after_cursor(src), Some(2));
789    }
790
791    #[test]
792    fn same_row_openers_collapse_to_one_level() {
793        // `foo(bar(` opens two `@indent` nodes on one row. Vim and
794        // every editor worth copying indent one level there, not two.
795        assert_eq!(
796            levels_after_cursor("fn f() {\n    foo(bar(|))\n}\n"),
797            Some(2)
798        );
799    }
800
801    #[test]
802    fn a_brace_inside_a_string_is_not_an_opener() {
803        // The case the lexical bridge gets wrong on purpose
804        // (`a_brace_inside_a_string_is_a_known_wrong_answer`). The
805        // tree knows it is a string literal, so the level stays 1 --
806        // this is the concrete improvement IN.2 buys.
807        let level = levels_after_cursor("fn f() {\n    println!(\"{\");|\n}\n");
808        assert_eq!(level, Some(1));
809    }
810
811    #[test]
812    fn existing_line_levels_dedent_the_closing_brace() {
813        let src = "fn f() {\n    x();\n}\n";
814        let snap = rust(src);
815        assert_eq!(tree_levels_for_line(&snap, 0), Some(0), "fn line");
816        assert_eq!(tree_levels_for_line(&snap, 1), Some(1), "body");
817        assert_eq!(tree_levels_for_line(&snap, 2), Some(0), "closing brace");
818    }
819
820    #[test]
821    fn existing_line_levels_handle_nesting() {
822        let src = "fn f() {\n    if x {\n        y();\n    }\n}\n";
823        let snap = rust(src);
824        assert_eq!(tree_levels_for_line(&snap, 1), Some(1), "if");
825        assert_eq!(tree_levels_for_line(&snap, 2), Some(2), "if body");
826        assert_eq!(tree_levels_for_line(&snap, 3), Some(1), "inner close");
827        assert_eq!(tree_levels_for_line(&snap, 4), Some(0), "outer close");
828    }
829
830    #[test]
831    fn existing_line_levels_are_independent_of_current_indentation() {
832        // `=` has to fix a scrambled file, so the answer must come
833        // from the tree rather than from what the line already has.
834        let scrambled = "fn f() {\nx();\n            y();\n}\n";
835        let snap = rust(scrambled);
836        assert_eq!(tree_levels_for_line(&snap, 1), Some(1));
837        assert_eq!(tree_levels_for_line(&snap, 2), Some(1));
838    }
839
840    #[test]
841    fn a_row_past_the_end_yields_none() {
842        assert_eq!(tree_levels_for_line(&rust("fn f() {}\n"), 99), None);
843    }
844
845    // ---- IN.3: the brace family ----
846
847    /// Parse `src` as `lang` and return the owned snapshot.
848    fn parsed(lang: Lang, src: &str) -> crate::syntax::SyntaxSnapshot {
849        let mut s = Syntax::for_language(lang)
850            .unwrap_or_else(|e| panic!("{lang:?} registry error: {e}"))
851            .unwrap_or_else(|| panic!("{lang:?} has no grammar"));
852        s.parse(src);
853        s.snapshot_owned()
854    }
855
856    /// Every language whose `indents_source` returns a query must
857    /// compile it. An invalid node kind does not degrade — `Query::new`
858    /// rejects it and the whole registry build fails for that language,
859    /// so this is the difference between "no indent for Go" and "Go
860    /// buffers do not parse".
861    #[test]
862    fn every_shipped_indents_query_compiles() {
863        let registry = crate::LangRegistry::standard().expect("registry builds");
864        let shipped = [
865            "rust",
866            // IN.3 — brace family.
867            "c",
868            "cpp",
869            "css",
870            "go",
871            "java",
872            "javascript",
873            "json",
874            "tsx",
875            "typescript",
876            // IN.4 — indent-sensitive + scripting.
877            "bash",
878            "lua",
879            "python",
880            "ruby",
881            // IN.5 — data + markup. `markdown` / `sql` absent by
882            // decision; see `markdown_and_sql_deliberately_ship_no_query`.
883            "html",
884            "toml",
885            "yaml",
886        ];
887        for name in shipped {
888            assert!(
889                indents_source(name).is_some(),
890                "{name} listed here but absent from indents_source"
891            );
892            assert!(
893                registry.indents_query(name).is_some(),
894                "{name} ships indents.scm but the registry has no compiled query"
895            );
896        }
897    }
898
899    /// One nesting case per language: a body line indents, and the
900    /// closing delimiter dedents back. This is the whole contract the
901    /// brace-family queries exist to satisfy, and it fails loudly if a
902    /// node name is right for the grammar but wrong for the construct.
903    #[test]
904    fn brace_family_indents_bodies_and_dedents_closers() {
905        /// One language's nesting fixture.
906        ///
907        /// A named struct rather than a 4-tuple of pairs: the tuple
908        /// tripped `type_complexity`, and `case.body_level` reads
909        /// better than `case.2.1` at the assertion site regardless.
910        struct Case {
911            lang: Lang,
912            src: &'static str,
913            body_row: u32,
914            body_level: i32,
915            closer_row: u32,
916            closer_level: i32,
917        }
918
919        // Levels are stated rather than assumed 1/0: Java's fixture
920        // nests a method inside a class, so its body sits at two and
921        // its inner closer at one. An assertion that hardcoded 1/0
922        // would have been wrong for the right reason and taught
923        // nothing.
924        let cases = [
925            Case {
926                lang: Lang::C,
927                src: "int f(void) {\n    g();\n}\n",
928                body_row: 1,
929                body_level: 1,
930                closer_row: 2,
931                closer_level: 0,
932            },
933            Case {
934                lang: Lang::Cpp,
935                src: "int f() {\n    g();\n}\n",
936                body_row: 1,
937                body_level: 1,
938                closer_row: 2,
939                closer_level: 0,
940            },
941            Case {
942                lang: Lang::Java,
943                src: "class A {\n    void f() {\n        g();\n    }\n}\n",
944                body_row: 2,
945                body_level: 2,
946                closer_row: 3,
947                closer_level: 1,
948            },
949            Case {
950                lang: Lang::JavaScript,
951                src: "function f() {\n    g();\n}\n",
952                body_row: 1,
953                body_level: 1,
954                closer_row: 2,
955                closer_level: 0,
956            },
957            Case {
958                lang: Lang::TypeScript,
959                src: "function f(): void {\n    g();\n}\n",
960                body_row: 1,
961                body_level: 1,
962                closer_row: 2,
963                closer_level: 0,
964            },
965            Case {
966                lang: Lang::Tsx,
967                src: "function f() {\n    g();\n}\n",
968                body_row: 1,
969                body_level: 1,
970                closer_row: 2,
971                closer_level: 0,
972            },
973            Case {
974                lang: Lang::Go,
975                src: "func f() {\n    g()\n}\n",
976                body_row: 1,
977                body_level: 1,
978                closer_row: 2,
979                closer_level: 0,
980            },
981            Case {
982                lang: Lang::Css,
983                src: "a {\n    color: red;\n}\n",
984                body_row: 1,
985                body_level: 1,
986                closer_row: 2,
987                closer_level: 0,
988            },
989            Case {
990                lang: Lang::Json,
991                src: "{\n    \"a\": 1\n}\n",
992                body_row: 1,
993                body_level: 1,
994                closer_row: 2,
995                closer_level: 0,
996            },
997        ];
998        for case in &cases {
999            let snap = parsed(case.lang, case.src);
1000            let lang = case.lang;
1001            assert_eq!(
1002                tree_levels_for_line(&snap, case.body_row),
1003                Some(case.body_level),
1004                "{lang:?}: body line level"
1005            );
1006            assert_eq!(
1007                tree_levels_for_line(&snap, case.closer_row),
1008                Some(case.closer_level),
1009                "{lang:?}: closing delimiter should dedent one level"
1010            );
1011        }
1012    }
1013
1014    /// Argument / parameter lists indent their continuations. This is
1015    /// the case the *lexical* bridge also gets right, so it is not
1016    /// proof of much on its own — but a query that captured only block
1017    /// bodies would fail it, and every language in the family lists its
1018    /// delimited-list nodes for exactly this.
1019    #[test]
1020    fn brace_family_indents_wrapped_argument_lists() {
1021        let cases: &[(Lang, &str)] = &[
1022            (Lang::C, "int f(void) {\n    g(\n        1);\n}\n"),
1023            (Lang::JavaScript, "function f() {\n    g(\n        1);\n}\n"),
1024            (Lang::Go, "func f() {\n    g(\n        1)\n}\n"),
1025        ];
1026        for (lang, src) in cases {
1027            let snap = parsed(*lang, src);
1028            assert_eq!(
1029                tree_levels_for_line(&snap, 2),
1030                Some(2),
1031                "{lang:?}: wrapped argument should sit under the call"
1032            );
1033        }
1034    }
1035
1036    // ---- IN.4: indent-sensitive + scripting ----
1037
1038    /// Word closers (`end`, `fi`, `done`, `esac`) dedent exactly as `}`
1039    /// does. This is the whole difference between the IN.4 group and
1040    /// the brace family, and a query that captured only the block
1041    /// nodes would leave every `end` hanging one level too deep.
1042    #[test]
1043    fn word_closers_dedent() {
1044        struct Case {
1045            lang: Lang,
1046            src: &'static str,
1047            body_row: u32,
1048            closer_row: u32,
1049        }
1050        let cases = [
1051            Case {
1052                lang: Lang::Lua,
1053                src: "if x then\n    y()\nend\n",
1054                body_row: 1,
1055                closer_row: 2,
1056            },
1057            Case {
1058                lang: Lang::Lua,
1059                src: "while x do\n    y()\nend\n",
1060                body_row: 1,
1061                closer_row: 2,
1062            },
1063            Case {
1064                lang: Lang::Ruby,
1065                src: "def f\n  g\nend\n",
1066                body_row: 1,
1067                closer_row: 2,
1068            },
1069            Case {
1070                lang: Lang::Bash,
1071                src: "if x; then\n    y\nfi\n",
1072                body_row: 1,
1073                closer_row: 2,
1074            },
1075            Case {
1076                lang: Lang::Bash,
1077                src: "for i in a; do\n    y\ndone\n",
1078                body_row: 1,
1079                closer_row: 2,
1080            },
1081        ];
1082        for case in &cases {
1083            let snap = parsed(case.lang, case.src);
1084            let lang = case.lang;
1085            let src = case.src;
1086            assert_eq!(
1087                tree_levels_for_line(&snap, case.body_row),
1088                Some(1),
1089                "{lang:?}: body should indent\n{src}"
1090            );
1091            assert_eq!(
1092                tree_levels_for_line(&snap, case.closer_row),
1093                Some(0),
1094                "{lang:?}: word closer should dedent\n{src}"
1095            );
1096        }
1097    }
1098
1099    /// Python's colon-suite indents, and the block has no closing token
1100    /// -- so the line *after* the suite returns to zero because the
1101    /// block node ended, not because a delimiter dedented it.
1102    #[test]
1103    fn python_suite_indents_and_ends_without_a_closer() {
1104        let snap = parsed(Lang::Python, "def f():\n    g()\n\nh()\n");
1105        assert_eq!(tree_levels_for_line(&snap, 1), Some(1), "suite body");
1106        assert_eq!(
1107            tree_levels_for_line(&snap, 3),
1108            Some(0),
1109            "after the suite, with no closing token involved"
1110        );
1111    }
1112
1113    /// Indentation inside a string is CONTENT. Applying the enclosing
1114    /// block's structural indent there would edit the string's value --
1115    /// a correctness bug, not a cosmetic one. The engine declines and
1116    /// the lexical bridge copies the previous line instead.
1117    #[test]
1118    fn the_engine_refuses_to_answer_inside_a_string() {
1119        // Python docstring: byte offset inside the triple-quoted body.
1120        let src = "def f():\n    \"\"\"doc\n    more\n    \"\"\"\n";
1121        let snap = parsed(Lang::Python, src);
1122        let inside = src.find("more").expect("fixture contains the marker");
1123        assert_eq!(
1124            tree_levels_for_new_line(&snap, inside),
1125            None,
1126            "a docstring's leading whitespace is data, not structure"
1127        );
1128
1129        // And the ordinary case still answers, so the guard is not
1130        // simply disabling the engine.
1131        let code_at = src.find("\"\"\"doc").expect("fixture marker");
1132        assert!(tree_levels_for_new_line(&snap, code_at - 1).is_some());
1133    }
1134
1135    // ---- IN.5: data + markup ----
1136
1137    #[test]
1138    fn data_and_markup_indent_nested_structure() {
1139        struct Case {
1140            lang: Lang,
1141            src: &'static str,
1142            row: u32,
1143            level: i32,
1144        }
1145        let cases = [
1146            // TOML: a wrapped array indents; keys under a `[table]`
1147            // header do NOT (that is the one judgement in the query).
1148            Case {
1149                lang: Lang::Toml,
1150                src: "[t]\na = [\n  1,\n]\n",
1151                row: 2,
1152                level: 1,
1153            },
1154            Case {
1155                lang: Lang::Toml,
1156                src: "[t]\na = 1\n",
1157                row: 1,
1158                level: 0,
1159            },
1160            // YAML: a nested mapping indents.
1161            Case {
1162                lang: Lang::Yaml,
1163                src: "a:\n  b: 1\n",
1164                row: 1,
1165                level: 1,
1166            },
1167            // HTML: children indent, the closing tag dedents.
1168            Case {
1169                lang: Lang::Html,
1170                src: "<div>\n  <p>x</p>\n</div>\n",
1171                row: 1,
1172                level: 1,
1173            },
1174            Case {
1175                lang: Lang::Html,
1176                src: "<div>\n  <p>x</p>\n</div>\n",
1177                row: 2,
1178                level: 0,
1179            },
1180        ];
1181        for case in &cases {
1182            let snap = parsed(case.lang, case.src);
1183            let lang = case.lang;
1184            let src = case.src;
1185            assert_eq!(
1186                tree_levels_for_line(&snap, case.row),
1187                Some(case.level),
1188                "{lang:?} row {}\n{src}",
1189                case.row
1190            );
1191        }
1192    }
1193
1194    /// Indentation inside a heredoc or a YAML block scalar is the
1195    /// VALUE. The engine must decline so the lexical bridge preserves
1196    /// whatever the user typed.
1197    ///
1198    /// IN.4's query header and commit message claimed this protection
1199    /// for heredocs before it existed — `cursor_in_string_scope`'s node
1200    /// list covered neither `heredoc_body` nor `block_scalar`. This
1201    /// test is what makes the claim true, and is written for both so a
1202    /// future edit to that list cannot quietly drop one.
1203    #[test]
1204    fn literal_blocks_are_data_and_the_engine_declines() {
1205        let bash = "cat <<EOF\n    indented data\nEOF\n";
1206        let snap = parsed(Lang::Bash, bash);
1207        let inside = bash.find("indented").expect("fixture marker");
1208        assert_eq!(
1209            tree_levels_for_new_line(&snap, inside),
1210            None,
1211            "a heredoc body's leading whitespace is data"
1212        );
1213
1214        let yaml = "script: |\n  line one\n    line two\nnext: 1\n";
1215        let snap = parsed(Lang::Yaml, yaml);
1216        let inside = yaml.find("line two").expect("fixture marker");
1217        assert_eq!(
1218            tree_levels_for_new_line(&snap, inside),
1219            None,
1220            "a YAML block scalar's leading whitespace is the value"
1221        );
1222
1223        // The guard must not swallow ordinary YAML: every plain scalar
1224        // is wrapped in a `string_scalar`, so a too-eager node list
1225        // would disable indentation for essentially all YAML.
1226        let plain = "a:\n  b: 1\n";
1227        let snap = parsed(Lang::Yaml, plain);
1228        let at = plain.find("b: 1").expect("fixture marker");
1229        assert!(
1230            tree_levels_for_new_line(&snap, at).is_some(),
1231            "plain scalars must NOT count as a string scope"
1232        );
1233    }
1234
1235    /// `markdown` and `sql` ship no query on purpose. Asserted so the
1236    /// absence reads as a decision rather than an oversight, and so
1237    /// adding one becomes a deliberate act that updates this test.
1238    #[test]
1239    fn markdown_and_sql_deliberately_ship_no_query() {
1240        assert!(indents_source("markdown").is_none());
1241        assert!(indents_source("sql").is_none());
1242    }
1243
1244    /// A brace inside a string is not an opener — the property that
1245    /// distinguishes the tree path from the lexical bridge, checked
1246    /// once per language family rather than only for Rust.
1247    #[test]
1248    fn brace_family_ignores_braces_inside_strings() {
1249        let cases: &[(Lang, &str)] = &[
1250            (Lang::C, "int f(void) {\n    g(\"{\");\n    h();\n}\n"),
1251            (
1252                Lang::JavaScript,
1253                "function f() {\n    g(\"{\");\n    h();\n}\n",
1254            ),
1255            (Lang::Go, "func f() {\n    g(\"{\")\n    h()\n}\n"),
1256        ];
1257        for (lang, src) in cases {
1258            let snap = parsed(*lang, src);
1259            assert_eq!(
1260                tree_levels_for_line(&snap, 2),
1261                Some(1),
1262                "{lang:?}: a brace in a string must not open a level"
1263            );
1264        }
1265    }
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270    use super::*;
1271
1272    fn unit() -> IndentUnit {
1273        IndentUnit::new(4, true, 4)
1274    }
1275
1276    fn syntax_indent(prev: Option<&str>, next: Option<&str>) -> String {
1277        indent_for_new_line(
1278            IndentMethod::Syntax,
1279            prev,
1280            next,
1281            unit(),
1282            BracketSyntax::BRACES,
1283        )
1284    }
1285
1286    fn keep_indent(prev: Option<&str>) -> String {
1287        indent_for_new_line(
1288            IndentMethod::Keep,
1289            prev,
1290            None,
1291            unit(),
1292            BracketSyntax::BRACES,
1293        )
1294    }
1295
1296    #[test]
1297    fn none_is_always_column_zero() {
1298        assert_eq!(
1299            indent_for_new_line(
1300                IndentMethod::None,
1301                Some("        deeply indented"),
1302                None,
1303                unit(),
1304                BracketSyntax::BRACES,
1305            ),
1306            ""
1307        );
1308    }
1309
1310    #[test]
1311    fn keep_copies_and_does_not_scan() {
1312        assert_eq!(keep_indent(Some("    x();")), "    ");
1313        // The distinguishing case: an unclosed opener. `keep` must NOT
1314        // add a level -- that is `smartindent`, not `autoindent`.
1315        assert_eq!(keep_indent(Some("    if x {")), "    ");
1316        assert_eq!(keep_indent(Some("no indent")), "");
1317        assert_eq!(keep_indent(None), "");
1318    }
1319
1320    #[test]
1321    fn syntax_fallback_adds_a_level_after_an_unclosed_opener() {
1322        assert_eq!(syntax_indent(Some("    if x {"), None), "        ");
1323        assert_eq!(syntax_indent(Some("fn f() {"), None), "    ");
1324        // Balanced on the line: no extra level.
1325        assert_eq!(syntax_indent(Some("    f(a);"), None), "    ");
1326        assert_eq!(syntax_indent(Some("    if x { y() }"), None), "    ");
1327    }
1328
1329    #[test]
1330    fn syntax_fallback_dedents_when_the_moved_tail_opens_with_a_closer() {
1331        // `<CR>` pressed just before a `}` that moves down with it.
1332        assert_eq!(syntax_indent(Some("        x();"), Some("}")), "    ");
1333        // Opener and closer together: they cancel.
1334        assert_eq!(syntax_indent(Some("    if x {"), Some("}")), "    ");
1335    }
1336
1337    #[test]
1338    fn a_language_with_no_brackets_degrades_to_keep() {
1339        // Prose: a brace in a sentence must not indent the next line.
1340        let prose = indent_for_new_line(
1341            IndentMethod::Syntax,
1342            Some("  a sentence with a { in it"),
1343            None,
1344            unit(),
1345            BracketSyntax::NONE,
1346        );
1347        assert_eq!(prose, "  ");
1348        assert_eq!(BracketSyntax::for_lang(Lang::Markdown), BracketSyntax::NONE);
1349        assert_eq!(BracketSyntax::for_lang(Lang::Plain), BracketSyntax::NONE);
1350        assert_eq!(BracketSyntax::for_lang(Lang::Rust), BracketSyntax::BRACES);
1351    }
1352
1353    #[test]
1354    fn indent_is_rendered_through_the_unit() {
1355        // noexpandtab: the copied indent comes back as a tab.
1356        let tabs = IndentUnit::new(4, false, 4);
1357        let out = indent_for_new_line(
1358            IndentMethod::Keep,
1359            Some("    x"),
1360            None,
1361            tabs,
1362            BracketSyntax::BRACES,
1363        );
1364        assert_eq!(out, "\t");
1365    }
1366
1367    #[test]
1368    fn a_tab_indented_previous_line_is_measured_in_columns() {
1369        // The previous line uses a tab; expandtab is on, so the new
1370        // line gets the equivalent in spaces.
1371        assert_eq!(keep_indent(Some("\tx();")), "    ");
1372    }
1373
1374    #[test]
1375    fn closing_more_than_opening_does_not_go_negative() {
1376        // A line that only closes: depth is negative, so no extra
1377        // level, and the copy is clamped at zero by `shift`.
1378        assert_eq!(syntax_indent(Some("}"), None), "");
1379        assert_eq!(syntax_indent(Some("    }"), Some("}")), "");
1380    }
1381
1382    #[test]
1383    fn a_brace_inside_a_string_is_a_known_wrong_answer() {
1384        // Documented in the module header: the lexical scan cannot see
1385        // string state. Asserted so the limitation is visible in the
1386        // suite rather than discovered later, and so IN.2's engine has
1387        // a concrete case to prove it improves on.
1388        assert_eq!(
1389            syntax_indent(Some(r#"    println!("{");"#), None),
1390            "        ",
1391            "lexical scan counts a brace in a string literal"
1392        );
1393    }
1394}