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}