on master help sign in
Hacky way to specify indent scopes per language via toml configs.

Can't do it via a scm query nicely because it returns an iterator over all the matches, whereas we want to traverse the tree ourselves. Can't extract the pattern data from a parsed query either. Oh well, toml files for now.

commit4a9d1163e04ba0fc29f8532afd35381b7df55e48 authorBlaž Hrastnik <blaz@mxxn.io> dateFri 14 May 2021, 19:21 (5 years ago) parent7260720
7 files changed+100−45
Cargo.lock+1 helix-core/Cargo.toml+1 helix-core/src/indent.rs+20−41 helix-core/src/syntax.rs+29−1 helix-term/src/commands.rs+14−3 helix-view/src/document.rs+5 runtime/queries/rust/indents.toml+30
Cargo.lock+1
@@ -291,6 +291,7 @@ dependencies = [
291291 "serde",
292292 "smallvec",
293293 "tendril",
294+ "toml",
294295 "tree-sitter",
295296 "unicode-segmentation",
296297 "unicode-width",
helix-core/Cargo.toml+1
@@ -22,5 +22,6 @@ once_cell = "1.4"
2222regex = "1"
2323
2424serde = { version = "1.0", features = ["derive"] }
25+toml = "0.5"
2526
2627etcetera = "0.3"
helix-core/src/indent.rs+20−41
@@ -1,6 +1,6 @@
11use crate::{
22 find_first_non_whitespace_char,
3− syntax::Syntax,
3+ syntax::{IndentQuery, LanguageConfiguration, Syntax},
44 tree_sitter::{Node, Tree},
55 Rope, RopeSlice,
66};
@@ -43,40 +43,11 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
4343 Some(node)
4444}
4545
46−fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
47− let mut increment: i32 = 0;
48−
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− ];
46+fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
47+ // NOTE: can't use contains() on query because of comparing Vec<String> and &str
48+ // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
7849
79− let outdent = &["where", "}", "]", ")"];
50+ let mut increment: i32 = 0;
8051
8152 let mut node = match node {
8253 Some(node) => node,
@@ -88,7 +59,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
8859 // if we're calculating indentation for a brand new line then the current node will become the
8960 // parent node. We need to take it's indentation level into account too.
9061 let node_kind = node.kind();
91− if newline && indent_scopes.contains(&node_kind) {
62+ if newline && query.indent.contains(node_kind) {
9263 increment += 1;
9364 }
9465
@@ -102,14 +73,14 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
10273 // }) <-- }) is two scopes
10374 let starts_same_line = start == prev_start;
10475
105− if outdent.contains(&node.kind()) && !starts_same_line {
76+ if query.outdent.contains(node.kind()) && !starts_same_line {
10677 // we outdent by skipping the rules for the current level and jumping up
10778 // node = parent;
10879 increment -= 1;
10980 // continue;
11081 }
11182
112− if indent_scopes.contains(&parent_kind) // && not_first_or_last_sibling
83+ if query.indent.contains(parent_kind) // && not_first_or_last_sibling
11384 && !starts_same_line
11485 {
11586 // println!("is_scope {}", parent_kind);
@@ -128,6 +99,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
12899}
129100
130101fn suggested_indent_for_line(
102+ language_config: &LanguageConfiguration,
131103 syntax: Option<&Syntax>,
132104 text: RopeSlice,
133105 line_num: usize,
@@ -137,7 +109,7 @@ fn suggested_indent_for_line(
137109 let current = indent_level_for_line(line, tab_width);
138110
139111 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);
141113 };
142114
143115 // if the line is blank, indent should be zero
@@ -148,18 +120,24 @@ fn suggested_indent_for_line(
148120// - it should return 0 when mass indenting stuff
149121// - it should look up the wrapper node and count it too when we press o/O
150122pub fn suggested_indent_for_pos(
123+ language_config: Option<&LanguageConfiguration>,
151124 syntax: Option<&Syntax>,
152125 text: RopeSlice,
153126 pos: usize,
154127 new_line: bool,
155128) -> 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+ ) {
157133 let byte_start = text.char_to_byte(pos);
158134 let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
159135
136+ // let config = load indentation query config from Syntax(should contain language_config)
137+
160138 // TODO: special case for comments
161139 // TODO: if preserve_leading_whitespace
162− calculate_indentation(node, new_line)
140+ calculate_indentation(query, node, new_line)
163141 } else {
164142 // TODO: heuristics for non-tree sitter grammars
165143 0
@@ -286,6 +264,7 @@ where
286264 tab_width: 4,
287265 unit: String::from(" "),
288266 }),
267+ indent_query: OnceCell::new(),
289268 }],
290269 });
291270
@@ -304,7 +283,7 @@ where
304283 let line = text.line(i);
305284 let indent = indent_level_for_line(line, tab_width);
306285 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),
308287 indent,
309288 "line {}: {}",
310289 i,
helix-core/src/syntax.rs+29−1
@@ -4,7 +4,7 @@ pub use helix_syntax::{get_language, get_language_name, Lang};
44use std::{
55 borrow::Cow,
66 cell::RefCell,
7− collections::HashMap,
7+ collections::{HashMap, HashSet},
88 path::{Path, PathBuf},
99 sync::Arc,
1010};
@@ -41,6 +41,9 @@ pub struct LanguageConfiguration {
4141 pub language_server: Option<LanguageServerConfiguration>,
4242 #[serde(skip_serializing_if = "Option::is_none")]
4343 pub indent: Option<IndentationConfiguration>,
44+
45+ #[serde(skip)]
46+ pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
4447}
4548
4649#[derive(Serialize, Deserialize)]
@@ -59,6 +62,17 @@ pub struct IndentationConfiguration {
5962 pub unit: String,
6063}
6164
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+
6276fn read_query(language: &str, filename: &str) -> String {
6377 static INHERITS_REGEX: Lazy<Regex> =
6478 Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
@@ -127,6 +141,20 @@ impl LanguageConfiguration {
127141 .clone()
128142 }
129143
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+
130158 pub fn scope(&self) -> &str {
131159 &self.scope
132160 }
helix-term/src/commands.rs+14−3
@@ -1171,7 +1171,13 @@ fn open(cx: &mut Context, open: Open) {
11711171 let index = doc.text().line_to_char(line).saturating_sub(1);
11721172
11731173 // 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+ );
11751181 let indent = doc.indent_unit().repeat(indent_level);
11761182 let mut text = String::with_capacity(1 + indent.len());
11771183 text.push('\n');
@@ -1649,8 +1655,13 @@ pub mod insert {
16491655 let curr = contents.char(pos);
16501656
16511657 // 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+ );
16541665 let indent = doc.indent_unit().repeat(indent_level);
16551666 let mut text = String::with_capacity(1 + indent.len());
16561667 text.push('\n');
helix-view/src/document.rs+5
@@ -328,6 +328,11 @@ impl Document {
328328 .map(|language| language.scope.as_str())
329329 }
330330
331+ #[inline]
332+ pub fn language_config(&self) -> Option<&LanguageConfiguration> {
333+ self.language.as_deref()
334+ }
335+
331336 #[inline]
332337 /// Current document version, incremented at each change.
333338 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+]