From 5e6716c89c0909bc374e26bedbba703427f9aa26 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= <blaz@mxxn.io>
Date: Mon, 22 Mar 2021 13:47:39 +0900
Subject: [PATCH] Add tab_width and indent_unit config.

---
 helix-core/src/indent.rs    | 29 +++++++++++++++++------------
 helix-core/src/syntax.rs    | 14 ++++++++++++++
 helix-term/src/commands.rs  | 21 +++++++++++++--------
 helix-term/src/ui/editor.rs |  4 ++--
 helix-view/src/document.rs  | 20 ++++++++++++++++++++
 helix-view/src/view.rs      |  4 ++--
 6 files changed, 68 insertions(+), 24 deletions(-)

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<Node>, 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<Option<Arc<HighlightConfiguration>>>,
     // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
     pub language_server_config: Option<LanguageServerConfiguration>,
+    pub indent_config: Option<IndentationConfiguration>,
 }
 
 pub struct LanguageServerConfiguration {
@@ -36,6 +37,11 @@ pub struct LanguageServerConfiguration {
     pub args: Vec<String>,
 }
 
+pub struct IndentationConfiguration {
+    pub tab_width: usize,
+    pub indent_unit: String,
+}
+
 impl LanguageConfiguration {
     pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
         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);