on master help sign in
Hacky way to specify indent scopes per language via toml configs.
4a9d116Blaž Hrastnik
Cargo.lock+1
@@ -291,6 +291,7 @@ dependencies = [
291 "serde",
291 "serde",
292 "smallvec",
292 "smallvec",
293 "tendril",
293 "tendril",
294+ "toml",
294 "tree-sitter",
295 "tree-sitter",
295 "unicode-segmentation",
296 "unicode-segmentation",
296 "unicode-width",
297 "unicode-width",
helix-core/Cargo.toml+1
@@ -22,5 +22,6 @@ once_cell = "1.4"
22regex = "1"
22regex = "1"
23
23
24serde = { version = "1.0", features = ["derive"] }
24serde = { version = "1.0", features = ["derive"] }
25+toml = "0.5"
25
26
26etcetera = "0.3"
27etcetera = "0.3"
helix-core/src/indent.rs+20−41
@@ -1,6 +1,6 @@
1use crate::{
1use crate::{
2 find_first_non_whitespace_char,
2 find_first_non_whitespace_char,
3− syntax::Syntax,
3+ syntax::{IndentQuery, LanguageConfiguration, Syntax},
4 tree_sitter::{Node, Tree},
4 tree_sitter::{Node, Tree},
5 Rope, RopeSlice,
5 Rope, RopeSlice,
6};
6};
@@ -43,40 +43,11 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
43 Some(node)
43 Some(node)
44}
44}
45
45
46−fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
46+fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
47− let mut increment: i32 = 0;
47+ // NOTE: can't use contains() on query because of comparing Vec<String> and &str
48−
48+ // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
49− // Hardcoded for rust for now
50− let indent_scopes = &[
51− "while_expression",
52− "for_expression",
53− "loop_expression",
54− "if_expression",
55− "if_let_expression",
56− // "match_expression",
57− // "match_arm",
58− "tuple_expression",
59− "array_expression",
60− // indent_except_first_scopes
61− "use_list",
62− "block",
63− "match_block",
64− "arguments",
65− "parameters",
66− "declaration_list",
67− "field_declaration_list",
68− "field_initializer_list",
69− "struct_pattern",
70− "tuple_pattern",
71− "enum_variant_list",
72− // "function_item",
73− // "closure_expression",
74− "binary_expression",
75− "field_expression",
76− "where_clause",
77− ];
78
49
79− let outdent = &["where", "}", "]", ")"];
50+ let mut increment: i32 = 0;
80
51
81 let mut node = match node {
52 let mut node = match node {
82 Some(node) => node,
53 Some(node) => node,
@@ -88,7 +59,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
88 // if we're calculating indentation for a brand new line then the current node will become the
59 // if we're calculating indentation for a brand new line then the current node will become the
89 // parent node. We need to take it's indentation level into account too.
60 // parent node. We need to take it's indentation level into account too.
90 let node_kind = node.kind();
61 let node_kind = node.kind();
91− if newline && indent_scopes.contains(&node_kind) {
62+ if newline && query.indent.contains(node_kind) {
92 increment += 1;
63 increment += 1;
93 }
64 }
94
65
@@ -102,14 +73,14 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
102 // }) <-- }) is two scopes
73 // }) <-- }) is two scopes
103 let starts_same_line = start == prev_start;
74 let starts_same_line = start == prev_start;
104
75
105− if outdent.contains(&node.kind()) && !starts_same_line {
76+ if query.outdent.contains(node.kind()) && !starts_same_line {
106 // we outdent by skipping the rules for the current level and jumping up
77 // we outdent by skipping the rules for the current level and jumping up
107 // node = parent;
78 // node = parent;
108 increment -= 1;
79 increment -= 1;
109 // continue;
80 // continue;
110 }
81 }
111
82
112− if indent_scopes.contains(&parent_kind) // && not_first_or_last_sibling
83+ if query.indent.contains(parent_kind) // && not_first_or_last_sibling
113 && !starts_same_line
84 && !starts_same_line
114 {
85 {
115 // println!("is_scope {}", parent_kind);
86 // println!("is_scope {}", parent_kind);
@@ -128,6 +99,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
128}
99}
129
100
130fn suggested_indent_for_line(
101fn suggested_indent_for_line(
102+ language_config: &LanguageConfiguration,
131 syntax: Option<&Syntax>,
103 syntax: Option<&Syntax>,
132 text: RopeSlice,
104 text: RopeSlice,
133 line_num: usize,
105 line_num: usize,
@@ -137,7 +109,7 @@ fn suggested_indent_for_line(
137 let current = indent_level_for_line(line, tab_width);
109 let current = indent_level_for_line(line, tab_width);
138
110
139 if let Some(start) = find_first_non_whitespace_char(text, line_num) {
111 if let Some(start) = find_first_non_whitespace_char(text, line_num) {
140− return suggested_indent_for_pos(syntax, text, start, false);
112+ return suggested_indent_for_pos(Some(language_config), syntax, text, start, false);
141 };
113 };
142
114
143 // if the line is blank, indent should be zero
115 // if the line is blank, indent should be zero
@@ -148,18 +120,24 @@ fn suggested_indent_for_line(
148// - it should return 0 when mass indenting stuff
120// - it should return 0 when mass indenting stuff
149// - it should look up the wrapper node and count it too when we press o/O
121// - it should look up the wrapper node and count it too when we press o/O
150pub fn suggested_indent_for_pos(
122pub fn suggested_indent_for_pos(
123+ language_config: Option<&LanguageConfiguration>,
151 syntax: Option<&Syntax>,
124 syntax: Option<&Syntax>,
152 text: RopeSlice,
125 text: RopeSlice,
153 pos: usize,
126 pos: usize,
154 new_line: bool,
127 new_line: bool,
155) -> usize {
128) -> usize {
156− if let Some(syntax) = syntax {
129+ if let (Some(query), Some(syntax)) = (
130+ language_config.and_then(|config| config.indent_query()),
131+ syntax,
132+ ) {
157 let byte_start = text.char_to_byte(pos);
133 let byte_start = text.char_to_byte(pos);
158 let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
134 let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
159
135
136+ // let config = load indentation query config from Syntax(should contain language_config)
137+
160 // TODO: special case for comments
138 // TODO: special case for comments
161 // TODO: if preserve_leading_whitespace
139 // TODO: if preserve_leading_whitespace
162− calculate_indentation(node, new_line)
140+ calculate_indentation(query, node, new_line)
163 } else {
141 } else {
164 // TODO: heuristics for non-tree sitter grammars
142 // TODO: heuristics for non-tree sitter grammars
165 0
143 0
@@ -286,6 +264,7 @@ where
286 tab_width: 4,
264 tab_width: 4,
287 unit: String::from(" "),
265 unit: String::from(" "),
288 }),
266 }),
267+ indent_query: OnceCell::new(),
289 }],
268 }],
290 });
269 });
291
270
@@ -304,7 +283,7 @@ where
304 let line = text.line(i);
283 let line = text.line(i);
305 let indent = indent_level_for_line(line, tab_width);
284 let indent = indent_level_for_line(line, tab_width);
306 assert_eq!(
285 assert_eq!(
307− suggested_indent_for_line(Some(&syntax), text, i, tab_width),
286+ suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
308 indent,
287 indent,
309 "line {}: {}",
288 "line {}: {}",
310 i,
289 i,
helix-core/src/syntax.rs+29−1
@@ -4,7 +4,7 @@ pub use helix_syntax::{get_language, get_language_name, Lang};
4use std::{
4use std::{
5 borrow::Cow,
5 borrow::Cow,
6 cell::RefCell,
6 cell::RefCell,
7− collections::HashMap,
7+ collections::{HashMap, HashSet},
8 path::{Path, PathBuf},
8 path::{Path, PathBuf},
9 sync::Arc,
9 sync::Arc,
10};
10};
@@ -41,6 +41,9 @@ pub struct LanguageConfiguration {
41 pub language_server: Option<LanguageServerConfiguration>,
41 pub language_server: Option<LanguageServerConfiguration>,
42 #[serde(skip_serializing_if = "Option::is_none")]
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub indent: Option<IndentationConfiguration>,
43 pub indent: Option<IndentationConfiguration>,
44+
45+ #[serde(skip)]
46+ pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
44}
47}
45
48
46#[derive(Serialize, Deserialize)]
49#[derive(Serialize, Deserialize)]
@@ -59,6 +62,17 @@ pub struct IndentationConfiguration {
59 pub unit: String,
62 pub unit: String,
60}
63}
61
64
65+#[derive(Serialize, Deserialize)]
66+#[serde(rename_all = "kebab-case")]
67+pub struct IndentQuery {
68+ #[serde(default)]
69+ #[serde(skip_serializing_if = "HashSet::is_empty")]
70+ pub indent: HashSet<String>,
71+ #[serde(default)]
72+ #[serde(skip_serializing_if = "HashSet::is_empty")]
73+ pub outdent: HashSet<String>,
74+}
75+
62fn read_query(language: &str, filename: &str) -> String {
76fn read_query(language: &str, filename: &str) -> String {
63 static INHERITS_REGEX: Lazy<Regex> =
77 static INHERITS_REGEX: Lazy<Regex> =
64 Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
78 Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
@@ -127,6 +141,20 @@ impl LanguageConfiguration {
127 .clone()
141 .clone()
128 }
142 }
129
143
144+ pub fn indent_query(&self) -> Option<&IndentQuery> {
145+ self.indent_query
146+ .get_or_init(|| {
147+ let language = get_language_name(self.language_id).to_ascii_lowercase();
148+
149+ let root = crate::runtime_dir();
150+ let path = root.join("queries").join(language).join("indents.toml");
151+
152+ let toml = std::fs::read(&path).ok()?;
153+ toml::from_slice(&toml).ok()
154+ })
155+ .as_ref()
156+ }
157+
130 pub fn scope(&self) -> &str {
158 pub fn scope(&self) -> &str {
131 &self.scope
159 &self.scope
132 }
160 }
helix-term/src/commands.rs+14−3
@@ -1171,7 +1171,13 @@ fn open(cx: &mut Context, open: Open) {
1171 let index = doc.text().line_to_char(line).saturating_sub(1);
1171 let index = doc.text().line_to_char(line).saturating_sub(1);
1172
1172
1173 // TODO: share logic with insert_newline for indentation
1173 // TODO: share logic with insert_newline for indentation
1174− let indent_level = indent::suggested_indent_for_pos(doc.syntax(), text, index, true);
1174+ let indent_level = indent::suggested_indent_for_pos(
1175+ doc.language_config(),
1176+ doc.syntax(),
1177+ text,
1178+ index,
1179+ true,
1180+ );
1175 let indent = doc.indent_unit().repeat(indent_level);
1181 let indent = doc.indent_unit().repeat(indent_level);
1176 let mut text = String::with_capacity(1 + indent.len());
1182 let mut text = String::with_capacity(1 + indent.len());
1177 text.push('\n');
1183 text.push('\n');
@@ -1649,8 +1655,13 @@ pub mod insert {
1649 let curr = contents.char(pos);
1655 let curr = contents.char(pos);
1650
1656
1651 // TODO: offset range.head by 1? when calculating?
1657 // TODO: offset range.head by 1? when calculating?
1652− let indent_level =
1653− indent::suggested_indent_for_pos(doc.syntax(), text, pos.saturating_sub(1), true);
1658+ let indent_level = indent::suggested_indent_for_pos(
1659+ doc.language_config(),
1660+ doc.syntax(),
1661+ text,
1662+ pos.saturating_sub(1),
1663+ true,
1664+ );
1654 let indent = doc.indent_unit().repeat(indent_level);
1665 let indent = doc.indent_unit().repeat(indent_level);
1655 let mut text = String::with_capacity(1 + indent.len());
1666 let mut text = String::with_capacity(1 + indent.len());
1656 text.push('\n');
1667 text.push('\n');
helix-view/src/document.rs+5
@@ -328,6 +328,11 @@ impl Document {
328 .map(|language| language.scope.as_str())
328 .map(|language| language.scope.as_str())
329 }
329 }
330
330
331+ #[inline]
332+ pub fn language_config(&self) -> Option<&LanguageConfiguration> {
333+ self.language.as_deref()
334+ }
335+
331 #[inline]
336 #[inline]
332 /// Current document version, incremented at each change.
337 /// Current document version, incremented at each change.
333 pub fn version(&self) -> i32 {
338 pub fn version(&self) -> i32 {
runtime/queries/rust/indents.tomlnew file+30
@@ -0,0 +1,30 @@
1+indent = [
2+ "while_expression",
3+ "for_expression",
4+ "loop_expression",
5+ "if_expression",
6+ "if_let_expression",
7+ "tuple_expression",
8+ "array_expression",
9+ "use_list",
10+ "block",
11+ "match_block",
12+ "arguments",
13+ "parameters",
14+ "declaration_list",
15+ "field_declaration_list",
16+ "field_initializer_list",
17+ "struct_pattern",
18+ "tuple_pattern",
19+ "enum_variant_list",
20+ "binary_expression",
21+ "field_expression",
22+ "where_clause"
23+]
24+
25+outdent = [
26+ "where",
27+ "}",
28+ "]",
29+ ")"
30+]