floem_editor_core/
indent.rs

1use lapce_xi_rope::Rope;
2
3use crate::{
4    buffer::{rope_text::RopeText, Buffer},
5    chars::{char_is_line_ending, char_is_whitespace},
6    cursor::CursorAffinity,
7    selection::Selection,
8};
9
10/// Enum representing indentation style.
11///
12/// Only values 1-8 are valid for the `Spaces` variant.
13#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
14pub enum IndentStyle {
15    Tabs,
16    Spaces(u8),
17}
18
19impl std::fmt::Display for IndentStyle {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            IndentStyle::Tabs => f.write_str("Tabs"),
23            IndentStyle::Spaces(spaces) => f.write_fmt(format_args!("{spaces} spaces")),
24        }
25    }
26}
27
28impl IndentStyle {
29    pub const LONGEST_INDENT: &'static str = "        "; // 8 spaces
30    pub const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
31
32    /// Creates an `IndentStyle` from an indentation string.
33    ///
34    /// For example, passing `"    "` (four spaces) will create `IndentStyle::Spaces(4)`.
35    #[allow(clippy::should_implement_trait)]
36    #[inline]
37    pub fn from_str(indent: &str) -> Self {
38        debug_assert!(!indent.is_empty() && indent.len() <= Self::LONGEST_INDENT.len());
39        if indent.starts_with(' ') {
40            IndentStyle::Spaces(indent.len() as u8)
41        } else {
42            IndentStyle::Tabs
43        }
44    }
45
46    #[inline]
47    pub fn as_str(&self) -> &'static str {
48        match *self {
49            IndentStyle::Tabs => "\t",
50            IndentStyle::Spaces(x) if x <= Self::LONGEST_INDENT.len() as u8 => {
51                Self::LONGEST_INDENT.split_at(x.into()).0
52            }
53            // Unsupported indentation style.  This should never happen,
54            // but just in case fall back to the default of 4 spaces
55            IndentStyle::Spaces(n) => {
56                debug_assert!(n > 0 && n <= Self::LONGEST_INDENT.len() as u8);
57                "    "
58            }
59        }
60    }
61}
62
63pub fn create_edit<'s>(buffer: &Buffer, offset: usize, indent: &'s str) -> (Selection, &'s str) {
64    let indent = if indent.starts_with('\t') {
65        indent
66    } else {
67        let (_, col) = buffer.offset_to_line_col(offset);
68        indent.split_at(indent.len() - col % indent.len()).0
69    };
70    (Selection::caret(offset, CursorAffinity::Backward), indent)
71}
72
73pub fn create_outdent<'s>(
74    buffer: &Buffer,
75    offset: usize,
76    indent: &'s str,
77) -> Option<(Selection, &'s str)> {
78    let (_, col) = buffer.offset_to_line_col(offset);
79    if col == 0 {
80        return None;
81    }
82
83    let start = if indent.starts_with('\t') {
84        offset - 1
85    } else {
86        let r = col % indent.len();
87        let r = if r == 0 { indent.len() } else { r };
88        offset - r
89    };
90
91    Some((
92        Selection::region(start, offset, CursorAffinity::Backward),
93        "",
94    ))
95}
96
97/// Attempts to detect the indentation style used in a document.
98///
99/// Returns the indentation style if the auto-detect confidence is
100/// reasonably high, otherwise returns `None`.
101pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
102    // Build a histogram of the indentation *increases* between
103    // subsequent lines, ignoring lines that are all whitespace.
104    //
105    // Index 0 is for tabs, the rest are 1-8 spaces.
106    let histogram: [usize; 9] = {
107        let mut histogram = [0; 9];
108        let mut prev_line_is_tabs = false;
109        let mut prev_line_leading_count = 0usize;
110
111        // Loop through the lines, checking for and recording indentation
112        // increases as we go.
113        let offset = document_text
114            .offset_of_line(document_text.line_of_offset(document_text.len()).min(1000));
115        'outer: for line in document_text.lines(..offset) {
116            let mut c_iter = line.chars();
117
118            // Is first character a tab or space?
119            let is_tabs = match c_iter.next() {
120                Some('\t') => true,
121                Some(' ') => false,
122
123                // Ignore blank lines.
124                Some(c) if char_is_line_ending(c) => continue,
125
126                _ => {
127                    prev_line_is_tabs = false;
128                    prev_line_leading_count = 0;
129                    continue;
130                }
131            };
132
133            // Count the line's total leading tab/space characters.
134            let mut leading_count = 1;
135            let mut count_is_done = false;
136            for c in c_iter {
137                match c {
138                    '\t' if is_tabs && !count_is_done => leading_count += 1,
139                    ' ' if !is_tabs && !count_is_done => leading_count += 1,
140
141                    // We stop counting if we hit whitespace that doesn't
142                    // qualify as indent or doesn't match the leading
143                    // whitespace, but we don't exit the loop yet because
144                    // we still want to determine if the line is blank.
145                    c if char_is_whitespace(c) => count_is_done = true,
146
147                    // Ignore blank lines.
148                    c if char_is_line_ending(c) => continue 'outer,
149
150                    _ => break,
151                }
152
153                // Bound the worst-case execution time for weird text files.
154                if leading_count > 256 {
155                    continue 'outer;
156                }
157            }
158
159            // If there was an increase in indentation over the previous
160            // line, update the histogram with that increase.
161            if (prev_line_is_tabs == is_tabs || prev_line_leading_count == 0)
162                && prev_line_leading_count < leading_count
163            {
164                if is_tabs {
165                    histogram[0] += 1;
166                } else {
167                    let amount = leading_count - prev_line_leading_count;
168                    if amount <= 8 {
169                        histogram[amount] += 1;
170                    }
171                }
172            }
173
174            // Store this line's leading whitespace info for use with
175            // the next line.
176            prev_line_is_tabs = is_tabs;
177            prev_line_leading_count = leading_count;
178        }
179
180        // Give more weight to tabs, because their presence is a very
181        // strong indicator.
182        histogram[0] *= 2;
183
184        histogram
185    };
186
187    // Find the most frequent indent, its frequency, and the frequency of
188    // the next-most frequent indent.
189    let indent = histogram
190        .iter()
191        .enumerate()
192        .max_by_key(|kv| kv.1)
193        .unwrap()
194        .0;
195    let indent_freq = histogram[indent];
196    let indent_freq_2 = *histogram
197        .iter()
198        .enumerate()
199        .filter(|kv| kv.0 != indent)
200        .map(|kv| kv.1)
201        .max()
202        .unwrap();
203
204    // Return the auto-detected result if we're confident enough in its
205    // accuracy, based on some heuristics.
206    if indent_freq >= 1 && (indent_freq_2 as f64 / indent_freq as f64) < 0.66 {
207        Some(match indent {
208            0 => IndentStyle::Tabs,
209            _ => IndentStyle::Spaces(indent as u8),
210        })
211    } else {
212        None
213    }
214}