on master help sign in
Add tab_width and indent_unit config.
5e6716cBlaž Hrastnik
helix-core/src/indent.rs+17−12
@@ -8,19 +8,17 @@ use crate::{
8/// To determine indentation of a newly inserted line, figure out the indentation at the last col
8/// To determine indentation of a newly inserted line, figure out the indentation at the last col
9/// of the previous line.
9/// of the previous line.
10
10
11−pub const TAB_WIDTH: usize = 4;
12−
13−fn indent_level_for_line(line: RopeSlice) -> usize {
11+fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
14 let mut len = 0;
12 let mut len = 0;
15 for ch in line.chars() {
13 for ch in line.chars() {
16 match ch {
14 match ch {
17− '\t' => len += TAB_WIDTH,
15+ '\t' => len += tab_width,
18 ' ' => len += 1,
16 ' ' => len += 1,
19 _ => break,
17 _ => break,
20 }
18 }
21 }
19 }
22
20
23− len / TAB_WIDTH
21+ len / tab_width
24}
22}
25
23
26/// Find the highest syntax node at position.
24/// Find the highest syntax node at position.
@@ -162,9 +160,14 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
162 increment as usize
160 increment as usize
163}
161}
164
162
165−fn suggested_indent_for_line(syntax: Option<&Syntax>, text: RopeSlice, line_num: usize) -> usize {
163+fn suggested_indent_for_line(
164+ syntax: Option<&Syntax>,
165+ text: RopeSlice,
166+ line_num: usize,
167+ tab_width: usize,
168+) -> usize {
166 let line = text.line(line_num);
169 let line = text.line(line_num);
167− let current = indent_level_for_line(line);
170+ let current = indent_level_for_line(line, tab_width);
168
171
169 if let Some(start) = find_first_non_whitespace_char(text, line_num) {
172 if let Some(start) = find_first_non_whitespace_char(text, line_num) {
170 return suggested_indent_for_pos(syntax, text, start, false);
173 return suggested_indent_for_pos(syntax, text, start, false);
@@ -202,13 +205,14 @@ mod test {
202
205
203 #[test]
206 #[test]
204 fn test_indent_level() {
207 fn test_indent_level() {
208+ let tab_width = 4;
205 let line = Rope::from(" fn new"); // 8 spaces
209 let line = Rope::from(" fn new"); // 8 spaces
206− assert_eq!(indent_level_for_line(line.slice(..)), 2);
210+ assert_eq!(indent_level_for_line(line.slice(..), tab_width), 2);
207 let line = Rope::from("\t\t\tfn new"); // 3 tabs
211 let line = Rope::from("\t\t\tfn new"); // 3 tabs
208− assert_eq!(indent_level_for_line(line.slice(..)), 3);
212+ assert_eq!(indent_level_for_line(line.slice(..), tab_width), 3);
209 // mixed indentation
213 // mixed indentation
210 let line = Rope::from("\t \tfn new"); // 1 tab, 4 spaces, tab
214 let line = Rope::from("\t \tfn new"); // 1 tab, 4 spaces, tab
211− assert_eq!(indent_level_for_line(line.slice(..)), 3);
215+ assert_eq!(indent_level_for_line(line.slice(..), tab_width), 3);
212 }
216 }
213
217
214 #[test]
218 #[test]
@@ -295,12 +299,13 @@ where
295 let highlight_config = language_config.highlight_config(&[]).unwrap();
299 let highlight_config = language_config.highlight_config(&[]).unwrap();
296 let syntax = Syntax::new(&doc, highlight_config.clone());
300 let syntax = Syntax::new(&doc, highlight_config.clone());
297 let text = doc.slice(..);
301 let text = doc.slice(..);
302+ let tab_width = 4;
298
303
299 for i in 0..doc.len_lines() {
304 for i in 0..doc.len_lines() {
300 let line = text.line(i);
305 let line = text.line(i);
301− let indent = indent_level_for_line(line);
306+ let indent = indent_level_for_line(line, tab_width);
302 assert_eq!(
307 assert_eq!(
303− suggested_indent_for_line(Some(&syntax), text, i),
308+ suggested_indent_for_line(Some(&syntax), text, i, tab_width),
304 indent,
309 indent,
305 "line {}: {}",
310 "line {}: {}",
306 i,
311 i,
helix-core/src/syntax.rs+14
@@ -29,6 +29,7 @@ pub struct LanguageConfiguration {
29 pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
29 pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
30 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
30 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
31 pub language_server_config: Option<LanguageServerConfiguration>,
31 pub language_server_config: Option<LanguageServerConfiguration>,
32+ pub indent_config: Option<IndentationConfiguration>,
32}
33}
33
34
34pub struct LanguageServerConfiguration {
35pub struct LanguageServerConfiguration {
@@ -36,6 +37,11 @@ pub struct LanguageServerConfiguration {
36 pub args: Vec<String>,
37 pub args: Vec<String>,
37}
38}
38
39
40+pub struct IndentationConfiguration {
41+ pub tab_width: usize,
42+ pub indent_unit: String,
43+}
44+
39impl LanguageConfiguration {
45impl LanguageConfiguration {
40 pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
46 pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
41 self.highlight_config
47 self.highlight_config
@@ -104,6 +110,10 @@ impl Loader {
104 command: "rust-analyzer".to_string(),
110 command: "rust-analyzer".to_string(),
105 args: vec![],
111 args: vec![],
106 }),
112 }),
113+ indent_config: Some(IndentationConfiguration {
114+ tab_width: 4,
115+ indent_unit: String::from(" "),
116+ }),
107 },
117 },
108 LanguageConfiguration {
118 LanguageConfiguration {
109 scope: "source.toml".to_string(),
119 scope: "source.toml".to_string(),
@@ -114,6 +124,10 @@ impl Loader {
114 path: "../helix-syntax/languages/tree-sitter-toml".into(),
124 path: "../helix-syntax/languages/tree-sitter-toml".into(),
115 roots: vec![],
125 roots: vec![],
116 language_server_config: None,
126 language_server_config: None,
127+ indent_config: Some(IndentationConfiguration {
128+ tab_width: 2,
129+ indent_unit: String::from(" "),
130+ }),
117 },
131 },
118 ];
132 ];
119
133
helix-term/src/commands.rs+13−8
@@ -1,6 +1,5 @@
1use helix_core::{
1use helix_core::{
2 comment, coords_at_pos, graphemes,
2 comment, coords_at_pos, graphemes,
3− indent::TAB_WIDTH,
4 movement::{self, Direction},
3 movement::{self, Direction},
5 object, pos_at_coords,
4 object, pos_at_coords,
6 regex::{self, Regex},
5 regex::{self, Regex},
@@ -835,7 +834,7 @@ pub fn open_below(cx: &mut Context) {
835 // TODO: share logic with insert_newline for indentation
834 // TODO: share logic with insert_newline for indentation
836 let indent_level =
835 let indent_level =
837 helix_core::indent::suggested_indent_for_pos(doc.syntax(), text, index, true);
836 helix_core::indent::suggested_indent_for_pos(doc.syntax(), text, index, true);
838− let indent = " ".repeat(TAB_WIDTH).repeat(indent_level);
837+ let indent = doc.indent_unit().repeat(indent_level);
839 let mut text = String::with_capacity(1 + indent.len());
838 let mut text = String::with_capacity(1 + indent.len());
840 text.push('\n');
839 text.push('\n');
841 text.push_str(&indent);
840 text.push_str(&indent);
@@ -1035,8 +1034,13 @@ pub mod insert {
1035 }
1034 }
1036
1035
1037 pub fn insert_tab(cx: &mut Context) {
1036 pub fn insert_tab(cx: &mut Context) {
1038− // TODO: tab should insert either \t or indent width spaces
1037+ let doc = cx.doc();
1039− insert_char(cx, '\t');
1038+ // TODO: round out to nearest indentation level (for example a line with 3 spaces should
1039+ // indent by one to reach 4 spaces).
1040+
1041+ let indent = Tendril::from(doc.indent_unit());
1042+ let transaction = Transaction::insert(doc.text(), doc.selection(), indent);
1043+ doc.apply(&transaction);
1040 }
1044 }
1041
1045
1042 pub fn insert_newline(cx: &mut Context) {
1046 pub fn insert_newline(cx: &mut Context) {
@@ -1045,7 +1049,7 @@ pub mod insert {
1045 let transaction = Transaction::change_by_selection(doc.text(), doc.selection(), |range| {
1049 let transaction = Transaction::change_by_selection(doc.text(), doc.selection(), |range| {
1046 let indent_level =
1050 let indent_level =
1047 helix_core::indent::suggested_indent_for_pos(doc.syntax(), text, range.head, true);
1051 helix_core::indent::suggested_indent_for_pos(doc.syntax(), text, range.head, true);
1048− let indent = " ".repeat(TAB_WIDTH).repeat(indent_level);
1052+ let indent = doc.indent_unit().repeat(indent_level);
1049 let mut text = String::with_capacity(1 + indent.len());
1053 let mut text = String::with_capacity(1 + indent.len());
1050 text.push('\n');
1054 text.push('\n');
1051 text.push_str(&indent);
1055 text.push_str(&indent);
@@ -1185,7 +1189,7 @@ pub fn indent(cx: &mut Context) {
1185 let lines = get_lines(doc);
1189 let lines = get_lines(doc);
1186
1190
1187 // Indent by one level
1191 // Indent by one level
1188− let indent = Tendril::from(" ".repeat(TAB_WIDTH));
1192+ let indent = Tendril::from(doc.indent_unit());
1189
1193
1190 let transaction = Transaction::change(
1194 let transaction = Transaction::change(
1191 doc.text(),
1195 doc.text(),
@@ -1202,6 +1206,7 @@ pub fn unindent(cx: &mut Context) {
1202 let doc = cx.doc();
1206 let doc = cx.doc();
1203 let lines = get_lines(doc);
1207 let lines = get_lines(doc);
1204 let mut changes = Vec::with_capacity(lines.len());
1208 let mut changes = Vec::with_capacity(lines.len());
1209+ let tab_width = doc.tab_width();
1205
1210
1206 for line_idx in lines {
1211 for line_idx in lines {
1207 let line = doc.text().line(line_idx);
1212 let line = doc.text().line(line_idx);
@@ -1210,11 +1215,11 @@ pub fn unindent(cx: &mut Context) {
1210 for ch in line.chars() {
1215 for ch in line.chars() {
1211 match ch {
1216 match ch {
1212 ' ' => width += 1,
1217 ' ' => width += 1,
1213− '\t' => width = (width / TAB_WIDTH + 1) * TAB_WIDTH,
1218+ '\t' => width = (width / tab_width + 1) * tab_width,
1214 _ => break,
1219 _ => break,
1215 }
1220 }
1216
1221
1217− if width >= TAB_WIDTH {
1222+ if width >= tab_width {
1218 break;
1223 break;
1219 }
1224 }
1220 }
1225 }
helix-term/src/ui/editor.rs+2−2
@@ -6,7 +6,6 @@ use crate::{
6};
6};
7
7
8use helix_core::{
8use helix_core::{
9− indent::TAB_WIDTH,
10 syntax::{self, HighlightEvent},
9 syntax::{self, HighlightEvent},
11 Position, Range,
10 Position, Range,
12};
11};
@@ -106,6 +105,7 @@ impl EditorView {
106 let mut spans = Vec::new();
105 let mut spans = Vec::new();
107 let mut visual_x = 0;
106 let mut visual_x = 0;
108 let mut line = 0u16;
107 let mut line = 0u16;
108+ let tab_width = view.doc.tab_width();
109
109
110 'outer: for event in highlights {
110 'outer: for event in highlights {
111 match event.unwrap() {
111 match event.unwrap() {
@@ -152,7 +152,7 @@ impl EditorView {
152 break 'outer;
152 break 'outer;
153 }
153 }
154 } else if grapheme == "\t" {
154 } else if grapheme == "\t" {
155− visual_x += (TAB_WIDTH as u16);
155+ visual_x += (tab_width as u16);
156 } else {
156 } else {
157 if visual_x >= viewport.width {
157 if visual_x >= viewport.width {
158 // if we're offscreen just keep going until we hit a new line
158 // if we're offscreen just keep going until we hit a new line
helix-view/src/document.rs+20
@@ -296,6 +296,26 @@ impl Document {
296 self.syntax.as_ref()
296 self.syntax.as_ref()
297 }
297 }
298
298
299+ /// Tab size in columns.
300+ pub fn tab_width(&self) -> usize {
301+ self.language
302+ .as_ref()
303+ .and_then(|config| config.indent_config.as_ref())
304+ .map(|config| config.tab_width)
305+ .unwrap_or(4) // fallback to 4 columns
306+ }
307+
308+ /// Returns a string containing a single level of indentation.
309+ pub fn indent_unit(&self) -> &str {
310+ self.language
311+ .as_ref()
312+ .and_then(|config| config.indent_config.as_ref())
313+ .map(|config| config.indent_unit.as_str())
314+ .unwrap_or(" ") // fallback to 2 spaces
315+
316+ // " ".repeat(TAB_WIDTH)
317+ }
318+
299 #[inline]
319 #[inline]
300 /// File path on disk.
320 /// File path on disk.
301 pub fn path(&self) -> Option<&PathBuf> {
321 pub fn path(&self) -> Option<&PathBuf> {
helix-view/src/view.rs+2−2
@@ -5,7 +5,6 @@ use std::borrow::Cow;
5use crate::Document;
5use crate::Document;
6use helix_core::{
6use helix_core::{
7 graphemes::{grapheme_width, RopeGraphemes},
7 graphemes::{grapheme_width, RopeGraphemes},
8− indent::TAB_WIDTH,
9 Position, RopeSlice,
8 Position, RopeSlice,
10};
9};
11use slotmap::DefaultKey as Key;
10use slotmap::DefaultKey as Key;
@@ -72,10 +71,11 @@ impl View {
72 let line_start = text.line_to_char(line);
71 let line_start = text.line_to_char(line);
73 let line_slice = text.slice(line_start..pos);
72 let line_slice = text.slice(line_start..pos);
74 let mut col = 0;
73 let mut col = 0;
74+ let tab_width = self.doc.tab_width();
75
75
76 for grapheme in RopeGraphemes::new(line_slice) {
76 for grapheme in RopeGraphemes::new(line_slice) {
77 if grapheme == "\t" {
77 if grapheme == "\t" {
78− col += TAB_WIDTH;
78+ col += tab_width;
79 } else {
79 } else {
80 let grapheme = Cow::from(grapheme);
80 let grapheme = Cow::from(grapheme);
81 col += grapheme_width(&grapheme);
81 col += grapheme_width(&grapheme);