on master help sign in
Move indent-style code into `helix_core::indent`.
commitf88d4c1e20d4dbc244599ad3f3a5f301bec239bf authorNathan Vegdahl <cessen@cessen.com> dateThu 29 Jul 2021, 12:10 (5 years ago) parente191a75
3 files changed+186−156
helix-core/src/indent.rs+168−1 helix-term/src/commands.rs+2−1 helix-view/src/document.rs+16−154
helix-core/src/indent.rs+168−1
@@ -1,10 +1,177 @@
11use crate::{
2+ chars::{char_is_line_ending, char_is_whitespace},
23 find_first_non_whitespace_char,
34 syntax::{IndentQuery, LanguageConfiguration, Syntax},
45 tree_sitter::Node,
5− RopeSlice,
6+ Rope, RopeSlice,
67};
78
9+/// Enum representing indentation style.
10+///
11+/// Only values 1-8 are valid for the `Spaces` variant.
12+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
13+pub enum IndentStyle {
14+ Tabs,
15+ Spaces(u8),
16+}
17+
18+impl IndentStyle {
19+ /// Creates an `IndentStyle` from an indentation string.
20+ ///
21+ /// For example, passing `" "` (four spaces) will create `IndentStyle::Spaces(4)`.
22+ #[allow(clippy::should_implement_trait)]
23+ #[inline]
24+ pub fn from_str(indent: &str) -> Self {
25+ // XXX: do we care about validating the input more than this? Probably not...?
26+ debug_assert!(!indent.is_empty() && indent.len() <= 8);
27+
28+ if indent.starts_with(' ') {
29+ IndentStyle::Spaces(indent.len() as u8)
30+ } else {
31+ IndentStyle::Tabs
32+ }
33+ }
34+
35+ #[inline]
36+ pub fn as_str(&self) -> &'static str {
37+ match *self {
38+ IndentStyle::Tabs => "\t",
39+ IndentStyle::Spaces(1) => " ",
40+ IndentStyle::Spaces(2) => " ",
41+ IndentStyle::Spaces(3) => " ",
42+ IndentStyle::Spaces(4) => " ",
43+ IndentStyle::Spaces(5) => " ",
44+ IndentStyle::Spaces(6) => " ",
45+ IndentStyle::Spaces(7) => " ",
46+ IndentStyle::Spaces(8) => " ",
47+
48+ // Unsupported indentation style. This should never happen,
49+ // but just in case fall back to two spaces.
50+ IndentStyle::Spaces(n) => {
51+ debug_assert!(n > 0 && n <= 8); // Always triggers. `debug_panic!()` wanted.
52+ " "
53+ }
54+ }
55+ }
56+}
57+
58+/// Attempts to detect the indentation style used in a document.
59+///
60+/// Returns the indentation style if the auto-detect confidence is
61+/// reasonably high, otherwise returns `None`.
62+pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
63+ // Build a histogram of the indentation *increases* between
64+ // subsequent lines, ignoring lines that are all whitespace.
65+ //
66+ // Index 0 is for tabs, the rest are 1-8 spaces.
67+ let histogram: [usize; 9] = {
68+ let mut histogram = [0; 9];
69+ let mut prev_line_is_tabs = false;
70+ let mut prev_line_leading_count = 0usize;
71+
72+ // Loop through the lines, checking for and recording indentation
73+ // increases as we go.
74+ 'outer: for line in document_text.lines().take(1000) {
75+ let mut c_iter = line.chars();
76+
77+ // Is first character a tab or space?
78+ let is_tabs = match c_iter.next() {
79+ Some('\t') => true,
80+ Some(' ') => false,
81+
82+ // Ignore blank lines.
83+ Some(c) if char_is_line_ending(c) => continue,
84+
85+ _ => {
86+ prev_line_is_tabs = false;
87+ prev_line_leading_count = 0;
88+ continue;
89+ }
90+ };
91+
92+ // Count the line's total leading tab/space characters.
93+ let mut leading_count = 1;
94+ let mut count_is_done = false;
95+ for c in c_iter {
96+ match c {
97+ '\t' if is_tabs && !count_is_done => leading_count += 1,
98+ ' ' if !is_tabs && !count_is_done => leading_count += 1,
99+
100+ // We stop counting if we hit whitespace that doesn't
101+ // qualify as indent or doesn't match the leading
102+ // whitespace, but we don't exit the loop yet because
103+ // we still want to determine if the line is blank.
104+ c if char_is_whitespace(c) => count_is_done = true,
105+
106+ // Ignore blank lines.
107+ c if char_is_line_ending(c) => continue 'outer,
108+
109+ _ => break,
110+ }
111+
112+ // Bound the worst-case execution time for weird text files.
113+ if leading_count > 256 {
114+ continue 'outer;
115+ }
116+ }
117+
118+ // If there was an increase in indentation over the previous
119+ // line, update the histogram with that increase.
120+ if (prev_line_is_tabs == is_tabs || prev_line_leading_count == 0)
121+ && prev_line_leading_count < leading_count
122+ {
123+ if is_tabs {
124+ histogram[0] += 1;
125+ } else {
126+ let amount = leading_count - prev_line_leading_count;
127+ if amount <= 8 {
128+ histogram[amount] += 1;
129+ }
130+ }
131+ }
132+
133+ // Store this line's leading whitespace info for use with
134+ // the next line.
135+ prev_line_is_tabs = is_tabs;
136+ prev_line_leading_count = leading_count;
137+ }
138+
139+ // Give more weight to tabs, because their presence is a very
140+ // strong indicator.
141+ histogram[0] *= 2;
142+
143+ histogram
144+ };
145+
146+ // Find the most frequent indent, its frequency, and the frequency of
147+ // the next-most frequent indent.
148+ let indent = histogram
149+ .iter()
150+ .enumerate()
151+ .max_by_key(|kv| kv.1)
152+ .unwrap()
153+ .0;
154+ let indent_freq = histogram[indent];
155+ let indent_freq_2 = *histogram
156+ .iter()
157+ .enumerate()
158+ .filter(|kv| kv.0 != indent)
159+ .map(|kv| kv.1)
160+ .max()
161+ .unwrap();
162+
163+ // Return the the auto-detected result if we're confident enough in its
164+ // accuracy, based on some heuristics.
165+ if indent_freq >= 1 && (indent_freq_2 as f64 / indent_freq as f64) < 0.66 {
166+ Some(match indent {
167+ 0 => IndentStyle::Tabs,
168+ _ => IndentStyle::Spaces(indent as u8),
169+ })
170+ } else {
171+ None
172+ }
173+}
174+
8175/// To determine indentation of a newly inserted line, figure out the indentation at the last col
9176/// of the previous line.
10177#[allow(dead_code)]
helix-term/src/commands.rs+2−1
@@ -1,5 +1,6 @@
11use helix_core::{
22 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
3+ indent::IndentStyle,
34 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
45 match_brackets,
56 movement::{self, Direction},
@@ -11,7 +12,7 @@ use helix_core::{
1112};
1213
1314use helix_view::{
14− document::{IndentStyle, Mode},
15+ document::Mode,
1516 editor::Action,
1617 input::KeyEvent,
1718 keyboard::KeyCode,
helix-view/src/document.rs+16−154
@@ -9,8 +9,8 @@ use std::str::FromStr;
99use std::sync::Arc;
1010
1111use helix_core::{
12− chars::{char_is_line_ending, char_is_whitespace},
1312 history::History,
13+ indent::{auto_detect_indent_style, IndentStyle},
1414 line_ending::auto_detect_line_ending,
1515 syntax::{self, LanguageConfiguration},
1616 ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
@@ -63,12 +63,6 @@ impl<'de> Deserialize<'de> for Mode {
6363 }
6464}
6565
66−#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
67−pub enum IndentStyle {
68− Tabs,
69− Spaces(u8),
70−}
71−
7266pub struct Document {
7367 pub(crate) id: DocumentId,
7468 text: Rope,
@@ -460,9 +454,7 @@ impl Document {
460454 doc.detect_language(theme, loader);
461455 }
462456
463− // Detect indentation style and line ending.
464− doc.detect_indent_style();
465− doc.line_ending = auto_detect_line_ending(&doc.text).unwrap_or(DEFAULT_LINE_ENDING);
457+ doc.detect_indent_and_line_ending();
466458
467459 Ok(doc)
468460 }
@@ -580,6 +572,18 @@ impl Document {
580572 }
581573 }
582574
575+ pub fn detect_indent_and_line_ending(&mut self) {
576+ self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
577+ IndentStyle::from_str(
578+ self.language
579+ .as_ref()
580+ .and_then(|config| config.indent.as_ref())
581+ .map_or(" ", |config| config.unit.as_str()), // Fallback to 2 spaces.
582+ )
583+ });
584+ self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
585+ }
586+
583587 /// Reload the document from its path.
584588 pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> {
585589 let encoding = &self.encoding;
@@ -598,9 +602,7 @@ impl Document {
598602 self.append_changes_to_history(view_id);
599603 self.reset_modified();
600604
601− // Detect indentation style and line ending.
602− self.detect_indent_style();
603− self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
605+ self.detect_indent_and_line_ending();
604606
605607 Ok(())
606608 }
@@ -619,132 +621,6 @@ impl Document {
619621 self.encoding
620622 }
621623
622− fn detect_indent_style(&mut self) {
623− // Build a histogram of the indentation *increases* between
624− // subsequent lines, ignoring lines that are all whitespace.
625− //
626− // Index 0 is for tabs, the rest are 1-8 spaces.
627− let histogram: [usize; 9] = {
628− let mut histogram = [0; 9];
629− let mut prev_line_is_tabs = false;
630− let mut prev_line_leading_count = 0usize;
631−
632− // Loop through the lines, checking for and recording indentation
633− // increases as we go.
634− 'outer: for line in self.text.lines().take(1000) {
635− let mut c_iter = line.chars();
636−
637− // Is first character a tab or space?
638− let is_tabs = match c_iter.next() {
639− Some('\t') => true,
640− Some(' ') => false,
641−
642− // Ignore blank lines.
643− Some(c) if char_is_line_ending(c) => continue,
644−
645− _ => {
646− prev_line_is_tabs = false;
647− prev_line_leading_count = 0;
648− continue;
649− }
650− };
651−
652− // Count the line's total leading tab/space characters.
653− let mut leading_count = 1;
654− let mut count_is_done = false;
655− for c in c_iter {
656− match c {
657− '\t' if is_tabs && !count_is_done => leading_count += 1,
658− ' ' if !is_tabs && !count_is_done => leading_count += 1,
659−
660− // We stop counting if we hit whitespace that doesn't
661− // qualify as indent or doesn't match the leading
662− // whitespace, but we don't exit the loop yet because
663− // we still want to determine if the line is blank.
664− c if char_is_whitespace(c) => count_is_done = true,
665−
666− // Ignore blank lines.
667− c if char_is_line_ending(c) => continue 'outer,
668−
669− _ => break,
670− }
671−
672− // Bound the worst-case execution time for weird text files.
673− if leading_count > 256 {
674− continue 'outer;
675− }
676− }
677−
678− // If there was an increase in indentation over the previous
679− // line, update the histogram with that increase.
680− if (prev_line_is_tabs == is_tabs || prev_line_leading_count == 0)
681− && prev_line_leading_count < leading_count
682− {
683− if is_tabs {
684− histogram[0] += 1;
685− } else {
686− let amount = leading_count - prev_line_leading_count;
687− if amount <= 8 {
688− histogram[amount] += 1;
689− }
690− }
691− }
692−
693− // Store this line's leading whitespace info for use with
694− // the next line.
695− prev_line_is_tabs = is_tabs;
696− prev_line_leading_count = leading_count;
697− }
698−
699− // Give more weight to tabs, because their presence is a very
700− // strong indicator.
701− histogram[0] *= 2;
702−
703− histogram
704− };
705−
706− // Find the most frequent indent, its frequency, and the frequency of
707− // the next-most frequent indent.
708− let indent = histogram
709− .iter()
710− .enumerate()
711− .max_by_key(|kv| kv.1)
712− .unwrap()
713− .0;
714− let indent_freq = histogram[indent];
715− let indent_freq_2 = *histogram
716− .iter()
717− .enumerate()
718− .filter(|kv| kv.0 != indent)
719− .map(|kv| kv.1)
720− .max()
721− .unwrap();
722−
723− // Use the auto-detected result if we're confident enough in its
724− // accuracy, based on some heuristics. Otherwise fall back to
725− // the language-based setting.
726− if indent_freq >= 1 && (indent_freq_2 as f64 / indent_freq as f64) < 0.66 {
727− // Use the auto-detected setting.
728− self.indent_style = match indent {
729− 0 => IndentStyle::Tabs,
730− _ => IndentStyle::Spaces(indent as u8),
731− };
732− } else {
733− // Fall back to language-based setting.
734− let indent = self
735− .language
736− .as_ref()
737− .and_then(|config| config.indent.as_ref())
738− .map_or(" ", |config| config.unit.as_str()); // fallback to 2 spaces
739−
740− self.indent_style = if indent.starts_with(' ') {
741− IndentStyle::Spaces(indent.len() as u8)
742− } else {
743− IndentStyle::Tabs
744− };
745− }
746− }
747−
748624 pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
749625 let path = canonicalize_path(path)?;
750626
@@ -1002,21 +878,7 @@ impl Document {
1002878 /// TODO: we might not need this function anymore, since the information
1003879 /// is conveniently available in `Document::indent_style` now.
1004880 pub fn indent_unit(&self) -> &'static str {
1005− match self.indent_style {
1006− IndentStyle::Tabs => "\t",
1007− IndentStyle::Spaces(1) => " ",
1008− IndentStyle::Spaces(2) => " ",
1009− IndentStyle::Spaces(3) => " ",
1010− IndentStyle::Spaces(4) => " ",
1011− IndentStyle::Spaces(5) => " ",
1012− IndentStyle::Spaces(6) => " ",
1013− IndentStyle::Spaces(7) => " ",
1014− IndentStyle::Spaces(8) => " ",
1015−
1016− // Unsupported indentation style. This should never happen,
1017− // but just in case fall back to two spaces.
1018− _ => " ",
1019− }
881+ self.indent_style.as_str()
1020882 }
1021883
1022884 #[inline]