sqruff_lib/utils/reflow/
reindent.rs

1use std::borrow::Cow;
2use std::mem::take;
3
4use ahash::{AHashMap, AHashSet};
5use itertools::{Itertools, chain, enumerate};
6use smol_str::SmolStr;
7use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
8use sqruff_lib_core::lint_fix::LintFix;
9use sqruff_lib_core::parser::segments::{ErasedSegment, SegmentBuilder, Tables};
10use strum_macros::EnumString;
11
12use super::elements::{ReflowBlock, ReflowElement, ReflowPoint, ReflowSequenceType};
13use super::helpers::fixes_from_results;
14use super::rebreak::{LinePosition, RebreakSpan, identify_rebreak_spans};
15use crate::core::rules::LintResult;
16use crate::utils::reflow::elements::IndentStats;
17
18fn has_untemplated_newline(point: &ReflowPoint) -> bool {
19    if !point
20        .class_types()
21        .intersects(const { &SyntaxSet::new(&[SyntaxKind::Newline, SyntaxKind::Placeholder]) })
22    {
23        return false;
24    }
25    point.segments().iter().any(|segment| {
26        segment.is_type(SyntaxKind::Newline)
27            && (segment
28                .get_position_marker()
29                .is_none_or(|position_marker| position_marker.is_literal()))
30    })
31}
32
33#[derive(Debug, Clone)]
34struct IndentPoint {
35    idx: usize,
36    indent_impulse: isize,
37    indent_trough: isize,
38    initial_indent_balance: isize,
39    last_line_break_idx: Option<usize>,
40    is_line_break: bool,
41    untaken_indents: Vec<isize>,
42}
43
44impl IndentPoint {
45    fn closing_indent_balance(&self) -> isize {
46        self.initial_indent_balance + self.indent_impulse
47    }
48}
49
50#[derive(Debug, Clone)]
51struct IndentLine {
52    initial_indent_balance: isize,
53    indent_points: Vec<IndentPoint>,
54}
55
56impl IndentLine {
57    pub(crate) fn is_all_comments(&self, elements: &ReflowSequenceType) -> bool {
58        self.block_segments(elements).all(|seg| {
59            matches!(
60                seg.get_type(),
61                SyntaxKind::InlineComment | SyntaxKind::BlockComment | SyntaxKind::Comment
62            )
63        })
64    }
65
66    fn block_segments<'a>(
67        &self,
68        elements: &'a ReflowSequenceType,
69    ) -> impl Iterator<Item = &'a ErasedSegment> {
70        self.blocks(elements).map(|it| it.segment())
71    }
72
73    fn blocks<'a>(
74        &self,
75        elements: &'a ReflowSequenceType,
76    ) -> impl Iterator<Item = &'a ReflowBlock> {
77        let slice = if self
78            .indent_points
79            .last()
80            .unwrap()
81            .last_line_break_idx
82            .is_none()
83        {
84            0..self.indent_points.last().unwrap().idx
85        } else {
86            self.indent_points.first().unwrap().idx..self.indent_points.last().unwrap().idx
87        };
88
89        elements[slice].iter().filter_map(ReflowElement::as_block)
90    }
91}
92
93impl IndentLine {
94    fn from_points(indent_points: Vec<IndentPoint>) -> Self {
95        let starting_balance = if indent_points.last().unwrap().last_line_break_idx.is_some() {
96            indent_points[0].closing_indent_balance()
97        } else {
98            0
99        };
100
101        IndentLine {
102            initial_indent_balance: starting_balance,
103            indent_points,
104        }
105    }
106
107    fn closing_balance(&self) -> isize {
108        self.indent_points.last().unwrap().closing_indent_balance()
109    }
110
111    fn opening_balance(&self) -> isize {
112        if self
113            .indent_points
114            .last()
115            .unwrap()
116            .last_line_break_idx
117            .is_none()
118        {
119            return 0;
120        }
121
122        self.indent_points[0].closing_indent_balance()
123    }
124
125    fn desired_indent_units(&self, forced_indents: &[usize]) -> isize {
126        let relevant_untaken_indents: usize = if self.indent_points[0].indent_trough != 0 {
127            self.indent_points[0]
128                .untaken_indents
129                .iter()
130                .filter(|&&i| {
131                    i <= self.initial_indent_balance
132                        - (self.indent_points[0].indent_impulse
133                            - self.indent_points[0].indent_trough)
134                })
135                .count()
136        } else {
137            self.indent_points[0].untaken_indents.len()
138        };
139
140        self.initial_indent_balance - relevant_untaken_indents as isize
141            + forced_indents.len() as isize
142    }
143}
144
145impl std::fmt::Display for IndentLine {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        let indent_points_str = self
148            .indent_points
149            .iter()
150            .map(|ip| {
151                format!(
152                    "iPt@{}({}, {}, {}, {:?}, {}, {:?})",
153                    ip.idx,
154                    ip.indent_impulse,
155                    ip.indent_trough,
156                    ip.initial_indent_balance,
157                    ip.last_line_break_idx,
158                    ip.is_line_break,
159                    ip.untaken_indents
160                )
161            })
162            .collect::<Vec<String>>()
163            .join(", ");
164
165        write!(
166            f,
167            "IndentLine(iib={}, ipts=[{}])",
168            self.initial_indent_balance, indent_points_str
169        )
170    }
171}
172
173fn revise_comment_lines(lines: &mut [IndentLine], elements: &ReflowSequenceType) {
174    let mut comment_line_buffer = Vec::new();
175    let mut changes = Vec::new();
176
177    for (idx, line) in enumerate(&mut *lines) {
178        if line.is_all_comments(elements) {
179            comment_line_buffer.push(idx);
180        } else {
181            for comment_line_idx in comment_line_buffer.drain(..) {
182                changes.push((comment_line_idx, line.initial_indent_balance));
183            }
184        }
185    }
186
187    let changes = changes.into_iter().chain(
188        comment_line_buffer
189            .into_iter()
190            .map(|comment_line_idx| (comment_line_idx, 0)),
191    );
192    for (comment_line_idx, initial_indent_balance) in changes {
193        lines[comment_line_idx].initial_indent_balance = initial_indent_balance;
194    }
195}
196
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum IndentUnit {
199    Tab,
200    Space(usize),
201}
202
203impl Default for IndentUnit {
204    fn default() -> Self {
205        IndentUnit::Space(4)
206    }
207}
208
209impl IndentUnit {
210    pub fn from_type_and_size(indent_type: &str, indent_size: usize) -> Self {
211        match indent_type {
212            "tab" => IndentUnit::Tab,
213            "space" => IndentUnit::Space(indent_size),
214            _ => unreachable!("Invalid indent type {}", indent_type),
215        }
216    }
217}
218
219pub fn construct_single_indent(indent_unit: IndentUnit) -> Cow<'static, str> {
220    match indent_unit {
221        IndentUnit::Tab => "\t".into(),
222        IndentUnit::Space(space_size) => " ".repeat(space_size).into(),
223    }
224}
225
226fn prune_untaken_indents(
227    untaken_indents: Vec<isize>,
228    incoming_balance: isize,
229    indent_stats: &IndentStats,
230    has_newline: bool,
231) -> Vec<isize> {
232    let new_balance_threshold = if indent_stats.trough < indent_stats.impulse {
233        incoming_balance + indent_stats.impulse + indent_stats.trough
234    } else {
235        incoming_balance + indent_stats.impulse
236    };
237
238    let mut pruned_untaken_indents: Vec<_> = untaken_indents
239        .iter()
240        .filter(|&x| x <= &new_balance_threshold)
241        .copied()
242        .collect();
243
244    if indent_stats.impulse > indent_stats.trough && !has_newline {
245        for i in indent_stats.trough..indent_stats.impulse {
246            let indent_val = incoming_balance + i + 1;
247
248            if !indent_stats
249                .implicit_indents
250                .contains(&(indent_val - incoming_balance))
251            {
252                pruned_untaken_indents.push(indent_val);
253            }
254        }
255    }
256
257    pruned_untaken_indents
258}
259
260fn update_crawl_balances(
261    untaken_indents: Vec<isize>,
262    incoming_balance: isize,
263    indent_stats: &IndentStats,
264    has_newline: bool,
265) -> (isize, Vec<isize>) {
266    let new_untaken_indents =
267        prune_untaken_indents(untaken_indents, incoming_balance, indent_stats, has_newline);
268    let new_balance = incoming_balance + indent_stats.impulse;
269
270    (new_balance, new_untaken_indents)
271}
272
273fn crawl_indent_points(
274    elements: &ReflowSequenceType,
275    allow_implicit_indents: bool,
276) -> Vec<IndentPoint> {
277    let mut acc = Vec::new();
278
279    let mut last_line_break_idx = None;
280    let mut indent_balance = 0;
281    let mut untaken_indents = Vec::new();
282    let mut cached_indent_stats = None;
283    let mut cached_point = None;
284
285    for (idx, elem) in enumerate(elements) {
286        if let ReflowElement::Point(elem) = elem {
287            let mut indent_stats =
288                IndentStats::from_combination(cached_indent_stats.clone(), elem.indent_impulse());
289
290            if !indent_stats.implicit_indents.is_empty() {
291                let mut unclosed_bracket = false;
292
293                if allow_implicit_indents
294                    && elements[idx + 1]
295                        .class_types()
296                        .contains(SyntaxKind::StartBracket)
297                {
298                    let depth = elements[idx + 1]
299                        .as_block()
300                        .unwrap()
301                        .depth_info()
302                        .stack_depth;
303
304                    let elems = &elements[idx + 1..];
305                    unclosed_bracket = elems.is_empty();
306
307                    for elem_j in elems {
308                        if let Some(elem_j) = elem_j.as_point() {
309                            if elem_j.num_newlines() > 0 {
310                                unclosed_bracket = true;
311                                break;
312                            }
313                        } else if elem_j.class_types().contains(SyntaxKind::EndBracket)
314                            && elem_j.as_block().unwrap().depth_info().stack_depth == depth
315                        {
316                            unclosed_bracket = false;
317                            break;
318                        } else {
319                            unclosed_bracket = true;
320                        }
321                    }
322                }
323
324                if unclosed_bracket || !allow_implicit_indents {
325                    indent_stats.implicit_indents = Default::default();
326                }
327            }
328
329            // Was there a cache?
330            if cached_indent_stats.is_some() {
331                let cached_point: &IndentPoint = cached_point.as_ref().unwrap();
332
333                if cached_point.is_line_break {
334                    acc.push(IndentPoint {
335                        idx: cached_point.idx,
336                        indent_impulse: indent_stats.impulse,
337                        indent_trough: indent_stats.trough,
338                        initial_indent_balance: indent_balance,
339                        last_line_break_idx: cached_point.last_line_break_idx,
340                        is_line_break: true,
341                        untaken_indents: take(&mut untaken_indents),
342                    });
343                    // Before zeroing, crystallise any effect on overall
344                    // balances.
345
346                    (indent_balance, untaken_indents) =
347                        update_crawl_balances(untaken_indents, indent_balance, &indent_stats, true);
348
349                    let implicit_indents = take(&mut indent_stats.implicit_indents);
350                    indent_stats = IndentStats {
351                        impulse: 0,
352                        trough: 0,
353                        implicit_indents,
354                    };
355                } else {
356                    acc.push(IndentPoint {
357                        idx: cached_point.idx,
358                        indent_impulse: 0,
359                        indent_trough: 0,
360                        initial_indent_balance: indent_balance,
361                        last_line_break_idx: cached_point.last_line_break_idx,
362                        is_line_break: false,
363                        untaken_indents: untaken_indents.clone(),
364                    });
365                }
366            }
367
368            // Reset caches.
369            cached_indent_stats = None;
370            cached_point = None;
371
372            // Do we have a newline?
373            let has_newline = has_untemplated_newline(elem) && Some(idx) != last_line_break_idx;
374
375            // Construct the point we may yield
376            let indent_point = IndentPoint {
377                idx,
378                indent_impulse: indent_stats.impulse,
379                indent_trough: indent_stats.trough,
380                initial_indent_balance: indent_balance,
381                last_line_break_idx,
382                is_line_break: has_newline,
383                untaken_indents: untaken_indents.clone(),
384            };
385
386            if has_newline {
387                last_line_break_idx = idx.into();
388            }
389
390            if elements[idx + 1].class_types().intersects(
391                const {
392                    &SyntaxSet::new(&[
393                        SyntaxKind::Comment,
394                        SyntaxKind::InlineComment,
395                        SyntaxKind::BlockComment,
396                    ])
397                },
398            ) {
399                cached_indent_stats = indent_stats.clone().into();
400                cached_point = indent_point.clone().into();
401
402                continue;
403            } else if has_newline
404                || indent_stats.impulse != 0
405                || indent_stats.trough != 0
406                || idx == 0
407                || elements[idx + 1].segments()[0].is_type(SyntaxKind::EndOfFile)
408            {
409                acc.push(indent_point);
410            }
411
412            (indent_balance, untaken_indents) =
413                update_crawl_balances(untaken_indents, indent_balance, &indent_stats, has_newline);
414        }
415    }
416
417    acc
418}
419
420fn map_line_buffers(
421    elements: &ReflowSequenceType,
422    allow_implicit_indents: bool,
423) -> (Vec<IndentLine>, Vec<usize>) {
424    let mut lines = Vec::new();
425    let mut point_buffer = Vec::new();
426    let mut previous_points = AHashMap::new();
427    let mut untaken_indent_locs = AHashMap::new();
428    let mut imbalanced_locs = Vec::new();
429
430    for indent_point in crawl_indent_points(elements, allow_implicit_indents) {
431        point_buffer.push(indent_point.clone());
432        previous_points.insert(indent_point.idx, indent_point.clone());
433
434        if !indent_point.is_line_break {
435            let indent_stats = elements[indent_point.idx]
436                .as_point()
437                .unwrap()
438                .indent_impulse();
439
440            if (indent_stats.implicit_indents.is_empty() || !allow_implicit_indents)
441                && indent_point.indent_impulse > indent_point.indent_trough
442            {
443                untaken_indent_locs.insert(
444                    indent_point.initial_indent_balance + indent_point.indent_impulse,
445                    indent_point.idx,
446                );
447            }
448
449            continue;
450        }
451
452        lines.push(IndentLine::from_points(point_buffer.clone()));
453
454        let following_class_types = elements[indent_point.idx + 1].class_types();
455        if indent_point.indent_trough != 0 && !following_class_types.contains(SyntaxKind::EndOfFile)
456        {
457            let passing_indents = Range::new(
458                indent_point.initial_indent_balance,
459                indent_point.initial_indent_balance + indent_point.indent_trough,
460                -1,
461            )
462            .reversed();
463
464            for i in passing_indents {
465                let Some(&loc) = untaken_indent_locs.get(&i) else {
466                    break;
467                };
468
469                if elements[loc + 1]
470                    .class_types()
471                    .contains(SyntaxKind::StartBracket)
472                {
473                    continue;
474                }
475
476                if point_buffer.iter().any(|ip| ip.idx == loc) {
477                    continue;
478                }
479
480                let mut _pt = None;
481                for j in loc..indent_point.idx {
482                    if let Some(pt) = previous_points.get(&j) {
483                        if pt.is_line_break {
484                            _pt = Some(pt);
485                            break;
486                        }
487                    }
488                }
489
490                let _pt = _pt.unwrap();
491
492                // Then check if all comments.
493                if (_pt.idx + 1..indent_point.idx).step_by(2).all(|k| {
494                    elements[k].class_types().intersects(
495                        const {
496                            &SyntaxSet::new(&[
497                                SyntaxKind::Comment,
498                                SyntaxKind::InlineComment,
499                                SyntaxKind::BlockComment,
500                            ])
501                        },
502                    )
503                }) {
504                    // It is all comments. Ignore it.
505                    continue;
506                }
507
508                imbalanced_locs.push(loc);
509            }
510        }
511
512        untaken_indent_locs
513            .retain(|&k, _| k <= indent_point.initial_indent_balance + indent_point.indent_trough);
514        point_buffer = vec![indent_point];
515    }
516
517    if point_buffer.len() > 1 {
518        lines.push(IndentLine::from_points(point_buffer));
519    }
520
521    (lines, imbalanced_locs)
522}
523
524fn deduce_line_current_indent(
525    elements: &ReflowSequenceType,
526    last_line_break_idx: Option<usize>,
527) -> SmolStr {
528    let mut indent_seg = None;
529
530    if elements[0].segments().is_empty() {
531        return "".into();
532    } else if let Some(last_line_break_idx) = last_line_break_idx {
533        indent_seg = elements[last_line_break_idx]
534            .as_point()
535            .unwrap()
536            .get_indent_segment();
537    } else if matches!(elements[0], ReflowElement::Point(_))
538        && elements[0].segments()[0]
539            .get_position_marker()
540            .is_some_and(|marker| marker.working_loc() == (1, 1))
541    {
542        if elements[0].segments()[0].is_type(SyntaxKind::Placeholder) {
543            unimplemented!()
544        } else {
545            for segment in elements[0].segments().iter().rev() {
546                if segment.is_type(SyntaxKind::Whitespace) && !segment.is_templated() {
547                    indent_seg = Some(segment.clone());
548                    break;
549                }
550            }
551
552            if let Some(ref seg) = indent_seg {
553                if !seg.is_type(SyntaxKind::Whitespace) {
554                    indent_seg = None;
555                }
556            }
557        }
558    }
559
560    let Some(indent_seg) = indent_seg else {
561        return "".into();
562    };
563
564    if indent_seg.is_type(SyntaxKind::Placeholder) {
565        unimplemented!()
566    } else if indent_seg.get_position_marker().is_none() || !indent_seg.is_templated() {
567        return indent_seg.raw().clone();
568    } else {
569        unimplemented!()
570    }
571}
572
573fn lint_line_starting_indent(
574    tables: &Tables,
575    elements: &mut ReflowSequenceType,
576    indent_line: &IndentLine,
577    single_indent: &str,
578    forced_indents: &[usize],
579) -> Vec<LintResult> {
580    let indent_points = &indent_line.indent_points;
581    // Set up the default anchor
582    let initial_point_idx = indent_points[0].idx;
583    let before = elements[initial_point_idx + 1].segments()[0].clone();
584
585    // Find initial indent, and deduce appropriate string indent.
586    let current_indent =
587        deduce_line_current_indent(elements, indent_points.last().unwrap().last_line_break_idx);
588    let initial_point = elements[initial_point_idx].as_point().unwrap();
589    let desired_indent_units = indent_line.desired_indent_units(forced_indents);
590    let desired_starting_indent = desired_indent_units
591        .try_into()
592        .map_or(String::new(), |n| single_indent.repeat(n));
593
594    if current_indent == desired_starting_indent {
595        return Vec::new();
596    }
597
598    if initial_point_idx > 0 && initial_point_idx < elements.len() - 1 {
599        if elements[initial_point_idx + 1].class_types().intersects(
600            const {
601                &SyntaxSet::new(&[
602                    SyntaxKind::Comment,
603                    SyntaxKind::BlockComment,
604                    SyntaxKind::InlineComment,
605                ])
606            },
607        ) {
608            let last_indent =
609                deduce_line_current_indent(elements, indent_points[0].last_line_break_idx);
610
611            if current_indent.len() == last_indent.len() {
612                return Vec::new();
613            }
614        }
615
616        if elements[initial_point_idx - 1]
617            .class_types()
618            .contains(SyntaxKind::BlockComment)
619            && elements[initial_point_idx + 1]
620                .class_types()
621                .contains(SyntaxKind::BlockComment)
622            && current_indent.len() > desired_starting_indent.len()
623        {
624            return Vec::new();
625        }
626    }
627
628    let (new_results, new_point) = if indent_points[0].idx == 0 && !indent_points[0].is_line_break {
629        let init_seg = &elements[indent_points[0].idx].segments()[0];
630        let fixes = if init_seg.is_type(SyntaxKind::Placeholder) {
631            unimplemented!()
632        } else {
633            initial_point
634                .segments()
635                .iter()
636                .cloned()
637                .map(LintFix::delete)
638                .collect_vec()
639        };
640
641        (
642            vec![LintResult::new(
643                initial_point.segments()[0].clone().into(),
644                fixes,
645                Some("First line should not be indented.".into()),
646                None,
647            )],
648            ReflowPoint::new(Vec::new()),
649        )
650    } else {
651        initial_point.indent_to(
652            tables,
653            &desired_starting_indent,
654            None,
655            before.into(),
656            None,
657            None,
658        )
659    };
660
661    elements[initial_point_idx] = new_point.into();
662
663    new_results
664}
665
666fn lint_line_untaken_positive_indents(
667    tables: &Tables,
668    elements: &mut [ReflowElement],
669    indent_line: &IndentLine,
670    single_indent: &str,
671    imbalanced_indent_locs: &[usize],
672) -> (Vec<LintResult>, Vec<usize>) {
673    // First check whether this line contains any of the untaken problem points.
674    for ip in &indent_line.indent_points {
675        if imbalanced_indent_locs.contains(&ip.idx) {
676            // Force it at the relevant position.
677            let desired_indent = single_indent
678                .repeat((ip.closing_indent_balance() - ip.untaken_indents.len() as isize) as usize);
679            let target_point = elements[ip.idx].as_point().unwrap();
680
681            let (results, new_point) = target_point.indent_to(
682                tables,
683                &desired_indent,
684                None,
685                Some(elements[ip.idx + 1].segments()[0].clone()),
686                Some("reflow.indent.imbalance"),
687                None,
688            );
689
690            elements[ip.idx] = ReflowElement::Point(new_point);
691            // Keep track of the indent we forced, by returning it.
692            return (results, vec![ip.closing_indent_balance() as usize]);
693        }
694    }
695
696    // If we don't close the line higher there won't be any.
697    let starting_balance = indent_line.opening_balance();
698    let last_ip = indent_line.indent_points.last().unwrap();
699    // Check whether it closes the opening indent.
700    if last_ip.initial_indent_balance + last_ip.indent_trough <= starting_balance {
701        return (vec![], vec![]);
702    }
703
704    // Account for the closing trough.
705    let mut closing_trough = last_ip.initial_indent_balance
706        + if last_ip.indent_trough == 0 {
707            last_ip.indent_impulse
708        } else {
709            last_ip.indent_trough
710        };
711
712    // Edge case: Adjust closing trough for trailing indents after comments
713    // disrupting closing trough.
714    let mut _bal = 0;
715    for elem in &elements[last_ip.idx + 1..] {
716        if let ReflowElement::Point(_) = elem {
717            let stats = elem.as_point().unwrap().indent_impulse();
718            // If it's positive, stop. We likely won't find enough negative to come.
719            if stats.impulse > 0 {
720                break;
721            }
722            closing_trough = _bal + stats.trough;
723            _bal += stats.impulse;
724        } else if !elem.class_types().intersects(
725            const {
726                &SyntaxSet::new(&[
727                    SyntaxKind::Comment,
728                    SyntaxKind::InlineComment,
729                    SyntaxKind::BlockComment,
730                ])
731            },
732        ) {
733            break;
734        }
735    }
736
737    // On the way up we're looking for whether the ending balance was an untaken
738    // indent or not.
739    if !indent_line
740        .indent_points
741        .last()
742        .unwrap()
743        .untaken_indents
744        .contains(&closing_trough)
745    {
746        // If the closing point doesn't correspond to an untaken indent within the line
747        // (i.e. it _was_ taken), then there won't be an appropriate place to
748        // force an indent.
749        return (vec![], vec![]);
750    }
751
752    // The closing indent balance *does* correspond to an untaken indent on this
753    // line. We *should* force a newline at that position.
754    let mut target_point_idx = 0;
755    let mut desired_indent = String::new();
756    for ip in &indent_line.indent_points {
757        if ip.closing_indent_balance() == closing_trough {
758            target_point_idx = ip.idx;
759            desired_indent = single_indent
760                .repeat((ip.closing_indent_balance() - ip.untaken_indents.len() as isize) as usize);
761            break;
762        }
763    }
764
765    let target_point = elements[target_point_idx].as_point().unwrap();
766
767    let (results, new_point) = target_point.indent_to(
768        tables,
769        &desired_indent,
770        None,
771        Some(elements[target_point_idx + 1].segments()[0].clone()),
772        Some("reflow.indent.positive"),
773        None,
774    );
775
776    elements[target_point_idx] = ReflowElement::Point(new_point);
777    // Keep track of the indent we forced, by returning it.
778    (results, vec![closing_trough as usize])
779}
780
781fn lint_line_untaken_negative_indents(
782    tables: &Tables,
783    elements: &mut ReflowSequenceType,
784    indent_line: &IndentLine,
785    single_indent: &str,
786    forced_indents: &[usize],
787) -> Vec<LintResult> {
788    let mut results = Vec::new();
789
790    if indent_line.closing_balance() >= indent_line.opening_balance() {
791        return Vec::new();
792    }
793
794    for ip in indent_line.indent_points.split_last().unwrap().1 {
795        if ip.is_line_break || ip.indent_impulse >= 0 {
796            continue;
797        }
798
799        if ip.initial_indent_balance + ip.indent_trough >= indent_line.opening_balance() {
800            continue;
801        }
802
803        let covered_indents: AHashSet<isize> = Range::new(
804            ip.initial_indent_balance,
805            ip.initial_indent_balance + ip.indent_trough,
806            -1,
807        )
808        .collect();
809
810        let untaken_indents: AHashSet<_> = ip
811            .untaken_indents
812            .iter()
813            .copied()
814            .collect::<AHashSet<_>>()
815            .difference(&forced_indents.iter().map(|it| *it as isize).collect())
816            .copied()
817            .collect();
818
819        if covered_indents.is_subset(&untaken_indents) {
820            continue;
821        }
822
823        if elements.get(ip.idx + 1).is_some_and(|elem| {
824            elem.class_types().intersects(
825                const { &SyntaxSet::new(&[SyntaxKind::StatementTerminator, SyntaxKind::Comma]) },
826            )
827        }) {
828            continue;
829        }
830
831        let desired_indent = single_indent.repeat(
832            (ip.closing_indent_balance() - ip.untaken_indents.len() as isize
833                + forced_indents.len() as isize)
834                .max(0) as usize,
835        );
836
837        let target_point = elements[ip.idx].as_point().unwrap();
838        let (mut new_results, new_point) = target_point.indent_to(
839            tables,
840            &desired_indent,
841            None,
842            elements[ip.idx + 1].segments()[0].clone().into(),
843            None,
844            "reflow.indent.negative".into(),
845        );
846        elements[ip.idx] = new_point.into();
847        results.append(&mut new_results);
848    }
849
850    results
851}
852
853fn lint_line_buffer_indents(
854    tables: &Tables,
855    elements: &mut ReflowSequenceType,
856    indent_line: IndentLine,
857    single_indent: &str,
858    forced_indents: &mut Vec<usize>,
859    imbalanced_indent_locs: &[usize],
860) -> Vec<LintResult> {
861    let mut results = Vec::new();
862
863    let mut new_results = lint_line_starting_indent(
864        tables,
865        elements,
866        &indent_line,
867        single_indent,
868        forced_indents,
869    );
870    results.append(&mut new_results);
871
872    let (mut new_results, mut new_indents) = lint_line_untaken_positive_indents(
873        tables,
874        elements,
875        &indent_line,
876        single_indent,
877        imbalanced_indent_locs,
878    );
879
880    if !new_results.is_empty() {
881        results.append(&mut new_results);
882        forced_indents.append(&mut new_indents);
883        return results;
884    }
885
886    results.extend(lint_line_untaken_negative_indents(
887        tables,
888        elements,
889        &indent_line,
890        single_indent,
891        forced_indents,
892    ));
893
894    forced_indents.retain(|&i| (i as isize) < indent_line.closing_balance());
895
896    results
897}
898
899pub fn lint_indent_points(
900    tables: &Tables,
901    elements: ReflowSequenceType,
902    single_indent: &str,
903    _skip_indentation_in: AHashSet<String>,
904    allow_implicit_indents: bool,
905) -> (ReflowSequenceType, Vec<LintResult>) {
906    let (mut lines, imbalanced_indent_locs) = map_line_buffers(&elements, allow_implicit_indents);
907
908    let mut results = Vec::new();
909    let mut elem_buffer = elements.clone();
910    let mut forced_indents = Vec::new();
911
912    revise_comment_lines(&mut lines, &elements);
913
914    for line in lines {
915        let line_results = lint_line_buffer_indents(
916            tables,
917            &mut elem_buffer,
918            line,
919            single_indent,
920            &mut forced_indents,
921            &imbalanced_indent_locs,
922        );
923
924        results.extend(line_results);
925    }
926
927    (elem_buffer, results)
928}
929
930fn source_char_len(elements: &[ReflowElement]) -> usize {
931    let mut char_len = 0;
932    let mut last_source_slice = None;
933
934    for seg in elements.iter().flat_map(|elem| elem.segments()) {
935        if seg.is_type(SyntaxKind::Indent) || seg.is_type(SyntaxKind::Dedent) {
936            continue;
937        }
938
939        let Some(pos_marker) = seg.get_position_marker() else {
940            break;
941        };
942
943        let source_slice = pos_marker.source_slice.clone();
944        let source_str = pos_marker.source_str();
945
946        if let Some(pos) = source_str.find('\n') {
947            char_len += pos;
948            break;
949        }
950
951        let slice_len = source_slice.end - source_slice.start;
952
953        if Some(source_slice.clone()) != last_source_slice {
954            if !seg.raw().is_empty() && slice_len == 0 {
955                char_len += seg.raw().chars().count();
956            } else if slice_len == 0 {
957                continue;
958            } else if pos_marker.is_literal() {
959                char_len += seg.raw().chars().count();
960                last_source_slice = Some(source_slice);
961            } else {
962                char_len += source_slice.end - source_slice.start;
963                last_source_slice = Some(source_slice);
964            }
965        }
966    }
967
968    char_len
969}
970
971fn rebreak_priorities(spans: Vec<RebreakSpan>) -> AHashMap<usize, usize> {
972    let mut rebreak_priority = AHashMap::with_capacity(spans.len());
973
974    for span in spans {
975        let rebreak_indices: &[usize] = match span.line_position {
976            LinePosition::Leading => &[span.start_idx - 1],
977            LinePosition::Trailing => &[span.end_idx + 1],
978            LinePosition::Alone => &[span.start_idx - 1, span.end_idx + 1],
979            _ => {
980                unimplemented!()
981            }
982        };
983
984        let span_raw = span.target.raw().to_uppercase();
985        let mut priority = 6;
986
987        if span_raw == "," {
988            priority = 1;
989        } else if span.target.is_type(SyntaxKind::AssignmentOperator) {
990            priority = 2;
991        } else if span_raw == "OR" {
992            priority = 3;
993        } else if span_raw == "AND" {
994            priority = 4;
995        } else if span.target.is_type(SyntaxKind::ComparisonOperator) {
996            priority = 5;
997        } else if ["*", "/", "%"].contains(&span_raw.as_str()) {
998            priority = 7;
999        }
1000
1001        for rebreak_idx in rebreak_indices {
1002            rebreak_priority.insert(*rebreak_idx, priority);
1003        }
1004    }
1005
1006    rebreak_priority
1007}
1008
1009type MatchedIndentsType = AHashMap<FloatTypeWrapper, Vec<usize>>;
1010
1011fn increment_balance(
1012    input_balance: isize,
1013    indent_stats: &IndentStats,
1014    elem_idx: usize,
1015) -> (isize, MatchedIndentsType) {
1016    let mut balance = input_balance;
1017    let mut matched_indents = AHashMap::new();
1018
1019    if indent_stats.trough < 0 {
1020        for b in (0..indent_stats.trough.abs()).step_by(1) {
1021            let key = FloatTypeWrapper::new((balance + -b) as f64);
1022            matched_indents
1023                .entry(key)
1024                .or_insert_with(Vec::new)
1025                .push(elem_idx);
1026        }
1027        balance += indent_stats.impulse;
1028    } else if indent_stats.impulse > 0 {
1029        for b in 0..indent_stats.impulse {
1030            let key = FloatTypeWrapper::new((balance + b + 1) as f64);
1031            matched_indents
1032                .entry(key)
1033                .or_insert_with(Vec::new)
1034                .push(elem_idx);
1035        }
1036        balance += indent_stats.impulse;
1037    }
1038
1039    (balance, matched_indents)
1040}
1041
1042fn match_indents(
1043    line_elements: ReflowSequenceType,
1044    rebreak_priorities: AHashMap<usize, usize>,
1045    newline_idx: usize,
1046    allow_implicit_indents: bool,
1047) -> MatchedIndentsType {
1048    let mut balance = 0;
1049    let mut matched_indents: MatchedIndentsType = AHashMap::new();
1050    let mut implicit_indents = AHashMap::new();
1051
1052    for (idx, e) in enumerate(&line_elements) {
1053        let ReflowElement::Point(point) = e else {
1054            continue;
1055        };
1056
1057        let indent_stats = point.indent_impulse();
1058
1059        let e_idx =
1060            (newline_idx as isize - line_elements.len() as isize + idx as isize + 1) as usize;
1061
1062        if !indent_stats.implicit_indents.is_empty() {
1063            implicit_indents.insert(e_idx, indent_stats.implicit_indents.clone());
1064        }
1065
1066        let nmi;
1067        (balance, nmi) = increment_balance(balance, indent_stats, e_idx);
1068        for (b, indices) in nmi {
1069            matched_indents.entry(b).or_default().extend(indices);
1070        }
1071
1072        let Some(&priority) = rebreak_priorities.get(&idx) else {
1073            continue;
1074        };
1075
1076        let balance = FloatTypeWrapper::new(balance as f64 + 0.5 + (priority as f64 / 100.0));
1077        matched_indents.entry(balance).or_default().push(e_idx);
1078    }
1079
1080    matched_indents.retain(|_key, value| value != &[newline_idx]);
1081
1082    if allow_implicit_indents {
1083        let keys: Vec<_> = matched_indents.keys().copied().collect();
1084        for indent_level in keys {
1085            let major_points: AHashSet<_> = matched_indents[&indent_level]
1086                .iter()
1087                .copied()
1088                .collect::<AHashSet<_>>()
1089                .difference(&AHashSet::from([newline_idx]))
1090                .copied()
1091                .collect::<AHashSet<_>>()
1092                .difference(&implicit_indents.keys().copied().collect::<AHashSet<_>>())
1093                .copied()
1094                .collect();
1095
1096            if major_points.is_empty() {
1097                matched_indents.remove(&indent_level);
1098            }
1099        }
1100    }
1101
1102    matched_indents
1103}
1104
1105#[derive(Clone, Copy, PartialEq, Debug, Default, Eq, EnumString)]
1106#[strum(serialize_all = "lowercase")]
1107pub enum TrailingComments {
1108    #[default]
1109    Before,
1110    After,
1111}
1112
1113fn fix_long_line_with_comment(
1114    tables: &Tables,
1115    line_buffer: &ReflowSequenceType,
1116    elements: &ReflowSequenceType,
1117    current_indent: &str,
1118    line_length_limit: usize,
1119    last_indent_idx: Option<usize>,
1120    trailing_comments: TrailingComments,
1121) -> (ReflowSequenceType, Vec<LintFix>) {
1122    if line_buffer
1123        .last()
1124        .unwrap()
1125        .segments()
1126        .last()
1127        .unwrap()
1128        .raw()
1129        .contains("noqa")
1130    {
1131        return (elements.clone(), Vec::new());
1132    }
1133
1134    if line_buffer
1135        .last()
1136        .unwrap()
1137        .segments()
1138        .last()
1139        .unwrap()
1140        .raw()
1141        .len()
1142        + current_indent.len()
1143        > line_length_limit
1144    {
1145        return (elements.clone(), Vec::new());
1146    }
1147
1148    let comment_seg = line_buffer.last().unwrap().segments().last().unwrap();
1149    let first_seg = line_buffer.first().unwrap().segments().first().unwrap();
1150    let last_elem_idx = elements
1151        .iter()
1152        .position(|elem| elem == line_buffer.last().unwrap())
1153        .unwrap();
1154
1155    if trailing_comments == TrailingComments::After {
1156        let mut elements = elements.clone();
1157        let anchor_point = line_buffer[line_buffer.len() - 2].as_point().unwrap();
1158        let (results, new_point) = anchor_point.indent_to(
1159            tables,
1160            current_indent,
1161            None,
1162            comment_seg.clone().into(),
1163            None,
1164            None,
1165        );
1166        elements.splice(
1167            last_elem_idx - 1..last_elem_idx,
1168            [new_point.into()].iter().cloned(),
1169        );
1170        return (elements, fixes_from_results(results.into_iter()).collect());
1171    }
1172
1173    let mut fixes = chain(
1174        Some(LintFix::delete(comment_seg.clone())),
1175        line_buffer[line_buffer.len() - 2]
1176            .segments()
1177            .iter()
1178            .filter(|ws| ws.is_type(SyntaxKind::Whitespace))
1179            .map(|ws| LintFix::delete(ws.clone())),
1180    )
1181    .collect_vec();
1182
1183    let new_point;
1184    let anchor;
1185    let prev_elems: Vec<ReflowElement>;
1186
1187    if let Some(idx) = last_indent_idx {
1188        new_point = ReflowPoint::new(vec![
1189            SegmentBuilder::newline(tables.next_id(), "\n"),
1190            SegmentBuilder::whitespace(tables.next_id(), current_indent),
1191        ]);
1192        prev_elems = elements[..=idx].to_vec();
1193        anchor = elements[idx + 1].segments()[0].clone();
1194    } else {
1195        new_point = ReflowPoint::new(vec![SegmentBuilder::newline(tables.next_id(), "\n")]);
1196        prev_elems = Vec::new();
1197        anchor = first_seg.clone();
1198    }
1199
1200    fixes.push(LintFix::create_before(
1201        anchor,
1202        chain(
1203            Some(comment_seg.clone()),
1204            new_point.segments().iter().cloned(),
1205        )
1206        .collect_vec(),
1207    ));
1208
1209    let elements: Vec<_> = prev_elems
1210        .into_iter()
1211        .chain(Some(line_buffer.last().unwrap().clone()))
1212        .chain(Some(new_point.into()))
1213        .chain(line_buffer.iter().take(line_buffer.len() - 2).cloned())
1214        .chain(elements.iter().skip(last_elem_idx + 1).cloned())
1215        .collect();
1216
1217    (elements, fixes)
1218}
1219
1220fn fix_long_line_with_fractional_targets(
1221    tables: &Tables,
1222    elements: &mut [ReflowElement],
1223    target_breaks: Vec<usize>,
1224    desired_indent: &str,
1225) -> Vec<LintResult> {
1226    let mut line_results = Vec::new();
1227
1228    for e_idx in target_breaks {
1229        let e = elements[e_idx].as_point().unwrap();
1230        let (new_results, new_point) = e.indent_to(
1231            tables,
1232            desired_indent,
1233            elements[e_idx - 1].segments().last().cloned(),
1234            elements[e_idx + 1].segments()[0].clone().into(),
1235            None,
1236            None,
1237        );
1238
1239        elements[e_idx] = new_point.into();
1240        line_results.extend(new_results);
1241    }
1242
1243    line_results
1244}
1245
1246fn fix_long_line_with_integer_targets(
1247    tables: &Tables,
1248    elements: &mut [ReflowElement],
1249    mut target_breaks: Vec<usize>,
1250    line_length_limit: usize,
1251    inner_indent: &str,
1252    outer_indent: &str,
1253) -> Vec<LintResult> {
1254    let mut line_results = Vec::new();
1255
1256    let mut purge_before = 0;
1257    for &e_idx in &target_breaks {
1258        let Some(pos_marker) = elements[e_idx + 1].segments()[0].get_position_marker() else {
1259            break;
1260        };
1261
1262        if pos_marker.working_line_pos > line_length_limit {
1263            break;
1264        }
1265
1266        let e = elements[e_idx].as_point().unwrap();
1267        if e.indent_impulse().trough < 0 {
1268            continue;
1269        }
1270
1271        purge_before = e_idx;
1272    }
1273
1274    target_breaks.retain(|&e_idx| e_idx >= purge_before);
1275
1276    for e_idx in target_breaks {
1277        let e = elements[e_idx].as_point().unwrap().clone();
1278        let indent_stats = e.indent_impulse();
1279
1280        let new_indent = if indent_stats.impulse < 0 {
1281            if elements[e_idx + 1].class_types().intersects(
1282                const { &SyntaxSet::new(&[SyntaxKind::StatementTerminator, SyntaxKind::Comma]) },
1283            ) {
1284                break;
1285            }
1286
1287            outer_indent
1288        } else {
1289            inner_indent
1290        };
1291
1292        let (new_results, new_point) = e.indent_to(
1293            tables,
1294            new_indent,
1295            elements[e_idx - 1].segments().last().cloned(),
1296            elements[e_idx + 1].segments().first().cloned(),
1297            None,
1298            None,
1299        );
1300
1301        elements[e_idx] = new_point.into();
1302        line_results.extend(new_results);
1303
1304        if indent_stats.trough < 0 {
1305            break;
1306        }
1307    }
1308
1309    line_results
1310}
1311
1312pub fn lint_line_length(
1313    tables: &Tables,
1314    elements: &ReflowSequenceType,
1315    root_segment: &ErasedSegment,
1316    single_indent: &str,
1317    line_length_limit: usize,
1318    allow_implicit_indents: bool,
1319    trailing_comments: TrailingComments,
1320) -> (ReflowSequenceType, Vec<LintResult>) {
1321    if line_length_limit == 0 {
1322        return (elements.clone(), Vec::new());
1323    }
1324
1325    let mut elem_buffer = elements.clone();
1326    let mut line_buffer = Vec::new();
1327    let mut results = Vec::new();
1328
1329    let mut last_indent_idx = None;
1330    for (i, elem) in enumerate(elements) {
1331        if elem
1332            .as_point()
1333            .filter(|point| {
1334                elem_buffer[i + 1]
1335                    .class_types()
1336                    .contains(SyntaxKind::EndOfFile)
1337                    || has_untemplated_newline(point)
1338            })
1339            .is_some()
1340        {
1341            // In either case we want to process this, so carry on.
1342        } else {
1343            line_buffer.push(elem.clone());
1344            continue;
1345        }
1346
1347        if line_buffer.is_empty() {
1348            continue;
1349        }
1350
1351        let current_indent = if let Some(last_indent_idx) = last_indent_idx {
1352            deduce_line_current_indent(&elem_buffer, Some(last_indent_idx))
1353        } else {
1354            "".into()
1355        };
1356
1357        let char_len = source_char_len(&line_buffer);
1358        let line_len = current_indent.len() + char_len;
1359
1360        let first_seg = line_buffer[0].segments()[0].clone();
1361        let line_no = first_seg.get_position_marker().unwrap().working_line_no;
1362
1363        if line_len <= line_length_limit {
1364            log::info!("Line #{line_no}. Length {line_len} <= {line_length_limit}. OK.")
1365        } else {
1366            let line_elements = chain(line_buffer.clone(), Some(elem.clone())).collect_vec();
1367            let mut fixes: Vec<LintFix> = Vec::new();
1368
1369            let mut combined_elements = line_elements.clone();
1370            combined_elements.push(elements[i + 1].clone());
1371
1372            let spans = identify_rebreak_spans(&combined_elements, root_segment);
1373            let rebreak_priorities = rebreak_priorities(spans);
1374
1375            let matched_indents =
1376                match_indents(line_elements, rebreak_priorities, i, allow_implicit_indents);
1377
1378            let desc = format!("Line is too long ({line_len} > {line_length_limit}).");
1379
1380            if line_buffer.len() > 1
1381                && line_buffer
1382                    .last()
1383                    .unwrap()
1384                    .segments()
1385                    .last()
1386                    .unwrap()
1387                    .is_type(SyntaxKind::InlineComment)
1388            {
1389                (elem_buffer, fixes) = fix_long_line_with_comment(
1390                    tables,
1391                    &line_buffer,
1392                    elements,
1393                    &current_indent,
1394                    line_length_limit,
1395                    last_indent_idx,
1396                    trailing_comments,
1397                );
1398            } else if matched_indents.is_empty() {
1399                log::debug!("Handling as unfixable line.");
1400            } else {
1401                log::debug!("Handling as normal line.");
1402                let target_balance = matched_indents
1403                    .keys()
1404                    .map(|k| k.into_f64())
1405                    .fold(f64::INFINITY, f64::min);
1406                let mut desired_indent = current_indent.to_string();
1407
1408                if target_balance >= 1.0 {
1409                    desired_indent += single_indent;
1410                }
1411
1412                let mut target_breaks =
1413                    matched_indents[&FloatTypeWrapper::new(target_balance)].clone();
1414
1415                if let Some(pos) = target_breaks.iter().position(|&x| x == i) {
1416                    target_breaks.remove(pos);
1417                }
1418
1419                let line_results = if target_balance % 1.0 == 0.0 {
1420                    fix_long_line_with_integer_targets(
1421                        tables,
1422                        &mut elem_buffer,
1423                        target_breaks,
1424                        line_length_limit,
1425                        &desired_indent,
1426                        &current_indent,
1427                    )
1428                } else {
1429                    fix_long_line_with_fractional_targets(
1430                        tables,
1431                        &mut elem_buffer,
1432                        target_breaks,
1433                        &desired_indent,
1434                    )
1435                };
1436
1437                fixes = fixes_from_results(line_results.into_iter()).collect();
1438            }
1439
1440            results.push(LintResult::new(first_seg.into(), fixes, desc.into(), None))
1441        }
1442
1443        line_buffer.clear();
1444        last_indent_idx = Some(i);
1445    }
1446
1447    (elem_buffer, results)
1448}
1449
1450#[derive(Default, Hash, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
1451struct FloatTypeWrapper(u64);
1452
1453impl FloatTypeWrapper {
1454    fn new(value: f64) -> Self {
1455        Self(value.to_bits())
1456    }
1457
1458    fn into_f64(self) -> f64 {
1459        f64::from_bits(self.0)
1460    }
1461}
1462
1463impl std::fmt::Debug for FloatTypeWrapper {
1464    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1465        write!(f, "{:?}", f64::from_bits(self.0))
1466    }
1467}
1468
1469impl std::fmt::Display for FloatTypeWrapper {
1470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1471        write!(f, "{:?}", f64::from_bits(self.0))
1472    }
1473}
1474
1475#[derive(Clone)]
1476pub(crate) struct Range {
1477    index: isize,
1478    start: isize,
1479    step: isize,
1480    length: isize,
1481}
1482
1483impl Range {
1484    pub(crate) fn new(start: isize, stop: isize, step: isize) -> Self {
1485        Self {
1486            index: 0,
1487            start,
1488            step,
1489            length: if step.is_negative() && start > stop {
1490                (start - stop - 1) / (-step) + 1
1491            } else if start < stop {
1492                if step.is_positive() && step == 1 {
1493                    stop - start
1494                } else {
1495                    (stop - start - 1) / step + 1
1496                }
1497            } else {
1498                0
1499            },
1500        }
1501    }
1502
1503    fn reversed(self) -> Self {
1504        let length = self.length;
1505        let stop = self.start - self.step;
1506        let start = stop + length * self.step;
1507        let step = -self.step;
1508
1509        Self {
1510            index: 0,
1511            start,
1512            step,
1513            length,
1514        }
1515    }
1516}
1517
1518impl Iterator for Range {
1519    type Item = isize;
1520
1521    fn next(&mut self) -> Option<Self::Item> {
1522        let index = self.index;
1523        self.index += 1;
1524        if index < self.length {
1525            Some(self.start + index * self.step)
1526        } else {
1527            None
1528        }
1529    }
1530}
1531
1532#[cfg(test)]
1533mod tests {
1534    use pretty_assertions::assert_eq;
1535    use sqruff_lib::core::test_functions::parse_ansi_string;
1536
1537    use super::{IndentLine, IndentPoint};
1538    use crate::utils::reflow::sequence::ReflowSequence;
1539
1540    #[test]
1541    fn test_reflow_point_get_indent() {
1542        let cases = [
1543            ("select 1", 1, None),
1544            ("select\n  1", 1, "  ".into()),
1545            ("select\n \n  \n   1", 1, "   ".into()),
1546        ];
1547
1548        for (raw_sql_in, elem_idx, indent_out) in cases {
1549            let root = parse_ansi_string(raw_sql_in);
1550            let config = <_>::default();
1551            let seq = ReflowSequence::from_root(root, &config);
1552            let elem = seq.elements()[elem_idx].as_point().unwrap();
1553
1554            assert_eq!(indent_out, elem.get_indent().as_deref());
1555        }
1556    }
1557
1558    #[test]
1559    fn test_reflow_desired_indent_units() {
1560        let cases: [(IndentLine, &[usize], isize); 7] = [
1561            // Trivial case of a first line.
1562            (
1563                IndentLine {
1564                    initial_indent_balance: 0,
1565                    indent_points: vec![IndentPoint {
1566                        idx: 0,
1567                        indent_impulse: 0,
1568                        indent_trough: 0,
1569                        initial_indent_balance: 0,
1570                        last_line_break_idx: None,
1571                        is_line_break: false,
1572                        untaken_indents: Vec::new(),
1573                    }],
1574                },
1575                &[],
1576                0,
1577            ),
1578            // Simple cases of a normal lines.
1579            (
1580                IndentLine {
1581                    initial_indent_balance: 3,
1582                    indent_points: vec![IndentPoint {
1583                        idx: 6,
1584                        indent_impulse: 0,
1585                        indent_trough: 0,
1586                        initial_indent_balance: 3,
1587                        last_line_break_idx: 1.into(),
1588                        is_line_break: true,
1589                        untaken_indents: Vec::new(),
1590                    }],
1591                },
1592                &[],
1593                3,
1594            ),
1595            (
1596                IndentLine {
1597                    initial_indent_balance: 3,
1598                    indent_points: vec![IndentPoint {
1599                        idx: 6,
1600                        indent_impulse: 0,
1601                        indent_trough: 0,
1602                        initial_indent_balance: 3,
1603                        last_line_break_idx: Some(1),
1604                        is_line_break: true,
1605                        untaken_indents: vec![1],
1606                    }],
1607                },
1608                &[],
1609                2,
1610            ),
1611            (
1612                IndentLine {
1613                    initial_indent_balance: 3,
1614                    indent_points: vec![IndentPoint {
1615                        idx: 6,
1616                        indent_impulse: 0,
1617                        indent_trough: 0,
1618                        initial_indent_balance: 3,
1619                        last_line_break_idx: Some(1),
1620                        is_line_break: true,
1621                        untaken_indents: vec![1, 2],
1622                    }],
1623                },
1624                &[],
1625                1,
1626            ),
1627            (
1628                IndentLine {
1629                    initial_indent_balance: 3,
1630                    indent_points: vec![IndentPoint {
1631                        idx: 6,
1632                        indent_impulse: 0,
1633                        indent_trough: 0,
1634                        initial_indent_balance: 3,
1635                        last_line_break_idx: Some(1),
1636                        is_line_break: true,
1637                        untaken_indents: vec![2],
1638                    }],
1639                },
1640                &[2], // Forced indent takes us back up.
1641                3,
1642            ),
1643            (
1644                IndentLine {
1645                    initial_indent_balance: 3,
1646                    indent_points: vec![IndentPoint {
1647                        idx: 6,
1648                        indent_impulse: 0,
1649                        indent_trough: 0,
1650                        initial_indent_balance: 3,
1651                        last_line_break_idx: Some(1),
1652                        is_line_break: true,
1653                        untaken_indents: vec![3],
1654                    }],
1655                },
1656                &[],
1657                2,
1658            ),
1659            (
1660                IndentLine {
1661                    initial_indent_balance: 3,
1662                    indent_points: vec![IndentPoint {
1663                        idx: 6,
1664                        indent_impulse: 0,
1665                        indent_trough: -1,
1666                        initial_indent_balance: 3,
1667                        last_line_break_idx: Some(1),
1668                        is_line_break: true,
1669                        untaken_indents: vec![3],
1670                    }],
1671                },
1672                &[],
1673                3,
1674            ),
1675        ];
1676
1677        for (indent_line, forced_indents, expected_units) in cases {
1678            assert_eq!(
1679                indent_line.desired_indent_units(forced_indents),
1680                expected_units
1681            );
1682        }
1683    }
1684}