diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 9b1241e5..775bc8ba 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -8,19 +8,17 @@ use crate::{ /// To determine indentation of a newly inserted line, figure out the indentation at the last col /// of the previous line. -pub const TAB_WIDTH: usize = 4; - -fn indent_level_for_line(line: RopeSlice) -> usize { +fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { let mut len = 0; for ch in line.chars() { match ch { - '\t' => len += TAB_WIDTH, + '\t' => len += tab_width, ' ' => len += 1, _ => break, } } - len / TAB_WIDTH + len / tab_width } /// Find the highest syntax node at position. @@ -162,9 +160,14 @@ fn calculate_indentation(node: Option, newline: bool) -> usize { increment as usize } -fn suggested_indent_for_line(syntax: Option<&Syntax>, text: RopeSlice, line_num: usize) -> usize { +fn suggested_indent_for_line( + syntax: Option<&Syntax>, + text: RopeSlice, + line_num: usize, + tab_width: usize, +) -> usize { let line = text.line(line_num); - let current = indent_level_for_line(line); + let current = indent_level_for_line(line, tab_width); if let Some(start) = find_first_non_whitespace_char(text, line_num) { return suggested_indent_for_pos(syntax, text, start, false); @@ -202,13 +205,14 @@ mod test { #[test] fn test_indent_level() { + let tab_width = 4; let line = Rope::from(" fn new"); // 8 spaces - assert_eq!(indent_level_for_line(line.slice(..)), 2); + assert_eq!(indent_level_for_line(line.slice(..), tab_width), 2); let line = Rope::from("\t\t\tfn new"); // 3 tabs - assert_eq!(indent_level_for_line(line.slice(..)), 3); + assert_eq!(indent_level_for_line(line.slice(..), tab_width), 3); // mixed indentation let line = Rope::from("\t \tfn new"); // 1 tab, 4 spaces, tab - assert_eq!(indent_level_for_line(line.slice(..)), 3); + assert_eq!(indent_level_for_line(line.slice(..), tab_width), 3); } #[test] @@ -295,12 +299,13 @@ where let highlight_config = language_config.highlight_config(&[]).unwrap(); let syntax = Syntax::new(&doc, highlight_config.clone()); let text = doc.slice(..); + let tab_width = 4; for i in 0..doc.len_lines() { let line = text.line(i); - let indent = indent_level_for_line(line); + let indent = indent_level_for_line(line, tab_width); assert_eq!( - suggested_indent_for_line(Some(&syntax), text, i), + suggested_indent_for_line(Some(&syntax), text, i, tab_width), indent, "line {}: {}", i, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index c352f8f2..63e39f8f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -29,6 +29,7 @@ pub struct LanguageConfiguration { pub(crate) highlight_config: OnceCell>>, // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 pub language_server_config: Option, + pub indent_config: Option, } pub struct LanguageServerConfiguration { @@ -36,6 +37,11 @@ pub struct LanguageServerConfiguration { pub args: Vec, } +pub struct IndentationConfiguration { + pub tab_width: usize, + pub indent_unit: String, +} + impl LanguageConfiguration { pub fn highlight_config(&self, scopes: &[String]) -> Option> { self.highlight_config @@ -104,6 +110,10 @@ impl Loader { command: "rust-analyzer".to_string(), args: vec![], }), + indent_config: Some(IndentationConfiguration { + tab_width: 4, + indent_unit: String::from(" "), + }), }, LanguageConfiguration { scope: "source.toml".to_string(), @@ -114,6 +124,10 @@ impl Loader { path: "../helix-syntax/languages/tree-sitter-toml".into(), roots: vec![], language_server_config: None, + indent_config: Some(IndentationConfiguration { + tab_width: 2, + indent_unit: String::from(" "), + }), }, ]; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3e60277c..e67708e7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,6 +1,5 @@ use helix_core::{ comment, coords_at_pos, graphemes, - indent::TAB_WIDTH, movement::{self, Direction}, object, pos_at_coords, regex::{self, Regex}, @@ -835,7 +834,7 @@ pub fn open_below(cx: &mut Context) { // TODO: share logic with insert_newline for indentation let indent_level = helix_core::indent::suggested_indent_for_pos(doc.syntax(), text, index, true); - let indent = " ".repeat(TAB_WIDTH).repeat(indent_level); + let indent = doc.indent_unit().repeat(indent_level); let mut text = String::with_capacity(1 + indent.len()); text.push('\n'); text.push_str(&indent); @@ -1035,8 +1034,13 @@ pub mod insert { } pub fn insert_tab(cx: &mut Context) { - // TODO: tab should insert either \t or indent width spaces - insert_char(cx, '\t'); + let doc = cx.doc(); + // TODO: round out to nearest indentation level (for example a line with 3 spaces should + // indent by one to reach 4 spaces). + + let indent = Tendril::from(doc.indent_unit()); + let transaction = Transaction::insert(doc.text(), doc.selection(), indent); + doc.apply(&transaction); } pub fn insert_newline(cx: &mut Context) { @@ -1045,7 +1049,7 @@ pub mod insert { let transaction = Transaction::change_by_selection(doc.text(), doc.selection(), |range| { let indent_level = helix_core::indent::suggested_indent_for_pos(doc.syntax(), text, range.head, true); - let indent = " ".repeat(TAB_WIDTH).repeat(indent_level); + let indent = doc.indent_unit().repeat(indent_level); let mut text = String::with_capacity(1 + indent.len()); text.push('\n'); text.push_str(&indent); @@ -1185,7 +1189,7 @@ pub fn indent(cx: &mut Context) { let lines = get_lines(doc); // Indent by one level - let indent = Tendril::from(" ".repeat(TAB_WIDTH)); + let indent = Tendril::from(doc.indent_unit()); let transaction = Transaction::change( doc.text(), @@ -1202,6 +1206,7 @@ pub fn unindent(cx: &mut Context) { let doc = cx.doc(); let lines = get_lines(doc); let mut changes = Vec::with_capacity(lines.len()); + let tab_width = doc.tab_width(); for line_idx in lines { let line = doc.text().line(line_idx); @@ -1210,11 +1215,11 @@ pub fn unindent(cx: &mut Context) { for ch in line.chars() { match ch { ' ' => width += 1, - '\t' => width = (width / TAB_WIDTH + 1) * TAB_WIDTH, + '\t' => width = (width / tab_width + 1) * tab_width, _ => break, } - if width >= TAB_WIDTH { + if width >= tab_width { break; } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 3ee9d446..c48dc97e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -6,7 +6,6 @@ use crate::{ }; use helix_core::{ - indent::TAB_WIDTH, syntax::{self, HighlightEvent}, Position, Range, }; @@ -106,6 +105,7 @@ impl EditorView { let mut spans = Vec::new(); let mut visual_x = 0; let mut line = 0u16; + let tab_width = view.doc.tab_width(); 'outer: for event in highlights { match event.unwrap() { @@ -152,7 +152,7 @@ impl EditorView { break 'outer; } } else if grapheme == "\t" { - visual_x += (TAB_WIDTH as u16); + visual_x += (tab_width as u16); } else { if visual_x >= viewport.width { // if we're offscreen just keep going until we hit a new line diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index e606ec3c..f6c7c70d 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -296,6 +296,26 @@ impl Document { self.syntax.as_ref() } + /// Tab size in columns. + pub fn tab_width(&self) -> usize { + self.language + .as_ref() + .and_then(|config| config.indent_config.as_ref()) + .map(|config| config.tab_width) + .unwrap_or(4) // fallback to 4 columns + } + + /// Returns a string containing a single level of indentation. + pub fn indent_unit(&self) -> &str { + self.language + .as_ref() + .and_then(|config| config.indent_config.as_ref()) + .map(|config| config.indent_unit.as_str()) + .unwrap_or(" ") // fallback to 2 spaces + + // " ".repeat(TAB_WIDTH) + } + #[inline] /// File path on disk. pub fn path(&self) -> Option<&PathBuf> { diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index b406b756..31a36047 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -5,7 +5,6 @@ use std::borrow::Cow; use crate::Document; use helix_core::{ graphemes::{grapheme_width, RopeGraphemes}, - indent::TAB_WIDTH, Position, RopeSlice, }; use slotmap::DefaultKey as Key; @@ -72,10 +71,11 @@ impl View { let line_start = text.line_to_char(line); let line_slice = text.slice(line_start..pos); let mut col = 0; + let tab_width = self.doc.tab_width(); for grapheme in RopeGraphemes::new(line_slice) { if grapheme == "\t" { - col += TAB_WIDTH; + col += tab_width; } else { let grapheme = Cow::from(grapheme); col += grapheme_width(&grapheme);