@@ -21,11 +21,18 @@ pub enum Mode {
24+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
25+pub enum IndentStyle {
24pub struct Document {
30pub struct Document {
25 // rope + selection
31 // rope + selection
26 pub(crate) id: DocumentId,
32 pub(crate) id: DocumentId,
27 text: Rope,
33 text: Rope,
28 pub(crate) selections: HashMap<ViewId, Selection>,
34 pub(crate) selections: HashMap<ViewId, Selection>,
35+ pub(crate) indent_style: IndentStyle,
30 path: Option<PathBuf>,
37 path: Option<PathBuf>,
@@ -149,6 +156,7 @@ impl Document {
149 path: None,
156 path: None,
151 selections: HashMap::default(),
158 selections: HashMap::default(),
159+ indent_style: IndentStyle::Spaces(4),
152 mode: Mode::Normal,
160 mode: Mode::Normal,
153 restore_cursor: false,
161 restore_cursor: false,
154 syntax: None,
162 syntax: None,
@@ -182,6 +190,7 @@ impl Document {
182 let mut doc = Self::new(doc);
190 let mut doc = Self::new(doc);
183 // set the path and try detecting the language
191 // set the path and try detecting the language
184 doc.set_path(&path)?;
192 doc.set_path(&path)?;
193+ doc.detect_indent_style();
@@ -265,6 +274,176 @@ impl Document {
277+ fn detect_indent_style(&mut self) {
278+ // Determine whether a character is a line break.
280+ // TODO: this is probably a generally useful utility function. Where
281+ // should we put it?
282+ fn char_is_linebreak(c: char) -> bool {
284+ '\u{000A}' | // LineFeed
285+ '\u{000B}' | // VerticalTab
286+ '\u{000C}' | // FormFeed
287+ '\u{000D}' | // CarriageReturn
288+ '\u{0085}' | // NextLine
289+ '\u{2028}' | // Line Separator
290+ '\u{2029}' // ParagraphSeparator
297+ // Determine whether a character qualifies as (non-line-break)
300+ // TODO: this is probably a generally useful utility function. Where
301+ // should we put it?
303+ // TODO: this is a naive binary categorization of whitespace
304+ // characters. For display, word wrapping, etc. we'll need a better
305+ // categorization based on e.g. breaking vs non-breaking spaces
306+ // and whether they're zero-width or not.
307+ pub fn char_is_whitespace(c: char) -> bool {
309+ //'\u{1680}' | // Ogham Space Mark (here for completeness, but usually displayed as a dash, not as whitespace)
310+ '\u{0009}' | // Character Tabulation
311+ '\u{0020}' | // Space
312+ '\u{00A0}' | // No-break Space
313+ '\u{180E}' | // Mongolian Vowel Separator
314+ '\u{202F}' | // Narrow No-break Space
315+ '\u{205F}' | // Medium Mathematical Space
316+ '\u{3000}' | // Ideographic Space
317+ '\u{FEFF}' // Zero Width No-break Space
320+ // En Quad, Em Quad, En Space, Em Space, Three-per-em Space,
321+ // Four-per-em Space, Six-per-em Space, Figure Space,
322+ // Punctuation Space, Thin Space, Hair Space, Zero Width Space.
323+ c if c >= '\u{2000}' && c <= '\u{200B}' => true,
329+ // Build a histogram of the indentation *increases* between
330+ // subsequent lines, ignoring lines that are all whitespace.
332+ // Index 0 is for tabs, the rest are 1-8 spaces.
333+ let histogram: [usize; 9] = {
334+ let mut histogram = [0; 9];
335+ let mut prev_line_indent = (false, 0usize); // (was_tabs, count)
337+ 'outer: for line in self.text.lines().take(1000) {
338+ let mut c_iter = line.chars();
340+ // Is first character a tab or space?
341+ let is_tabs = match c_iter.next() {
345+ // Ignore blank lines.
346+ Some(c) if char_is_linebreak(c) => continue 'outer,
349+ prev_line_indent = (false, 0usize);
354+ // Count the total leading tab/space characters.
356+ let mut count_is_done = false;
359+ '\t' if is_tabs && !count_is_done => count += 1,
360+ ' ' if !is_tabs && !count_is_done => count += 1,
362+ // We stop counting if we hit whitespace that doesn't
363+ // qualify as indent or doesn't match the leading
364+ // whitespace, but we don't exit the loop yet because
365+ // we still want to determine if the line is blank.
366+ c if char_is_whitespace(c) => count_is_done = true,
368+ // Ignore blank lines.
369+ c if char_is_linebreak(c) => continue 'outer,
374+ // Bound the worst-case execution time for weird text files.
381+ if (prev_line_indent.0 == is_tabs || prev_line_indent.1 == 0)
382+ && prev_line_indent.1 < count
387+ let amount = count - prev_line_indent.1;
389+ histogram[amount] += 1;
394+ // Store data for use with the next line.
395+ prev_line_indent = (is_tabs, count);
398+ // Give more weight to tabs, because their presence is a very
399+ // strong indicator.
405+ // Find the most frequent indent, its frequency, and the frequency of
406+ // the next-most frequent indent.
407+ let indent = histogram
410+ .max_by_key(|kv| kv.1)
413+ let indent_freq = histogram[indent];
414+ let indent_freq_2 = *histogram
417+ .filter(|kv| kv.0 != indent)
422+ // Use the auto-detected result if we're confident enough in its
423+ // accuracy, based on some heuristics. Otherwise fall back to
424+ // the language-based setting.
425+ if indent_freq >= 1 && (indent_freq_2 as f64 / indent_freq as f64) < 0.66 {
426+ // Use the auto-detected setting.
427+ self.indent_style = match indent {
428+ 0 => IndentStyle::Tabs,
429+ _ => IndentStyle::Spaces(indent as u8),
432+ // Fall back to language-based setting.
436+ .and_then(|config| config.indent.as_ref())
437+ .map_or(" ", |config| config.unit.as_str()); // fallback to 2 spaces
439+ self.indent_style = if indent.starts_with(" ") {
440+ IndentStyle::Spaces(indent.len() as u8)
268 pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
447 pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
269 let path = canonicalize_path(path)?;
448 let path = canonicalize_path(path)?;
@@ -507,13 +686,25 @@ impl Document {
509 /// Returns a string containing a single level of indentation.
688 /// Returns a string containing a single level of indentation.
690+ /// TODO: we might not need this function anymore, since the information
691+ /// is conveniently available in `Document::indent_style` now.
510 pub fn indent_unit(&self) -> &str {
692 pub fn indent_unit(&self) -> &str {
511− self.language
693+ match self.indent_style {
512− .as_ref()
694+ IndentStyle::Tabs => "\t",
513− .and_then(|config| config.indent.as_ref())
695+ IndentStyle::Spaces(1) => " ",
514− .map_or(" ", |config| config.unit.as_str()) // fallback to 2 spaces
696+ IndentStyle::Spaces(2) => " ",
515−
697+ IndentStyle::Spaces(3) => " ",
516− // " ".repeat(TAB_WIDTH)
698+ IndentStyle::Spaces(4) => " ",
699+ IndentStyle::Spaces(5) => " ",
700+ IndentStyle::Spaces(6) => " ",
701+ IndentStyle::Spaces(7) => " ",
702+ IndentStyle::Spaces(8) => " ",
704+ // Unsupported indentation style. This should never happen,
705+ // but just in case fall back to two spaces.
519 #[inline]
710 #[inline]