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