1use crate::model::buffer::Buffer;
50use once_cell::sync::Lazy;
51use regex::Regex;
52use std::collections::HashMap;
53use std::sync::{Arc, RwLock};
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum Family {
59 CurlyBrace,
62 Python,
64 RubyLike,
66 LuaLike,
68 BashLike,
70 FishLike,
72 PascalLike,
74 SmaliLike,
77}
78
79#[derive(Debug, Clone, Default)]
82pub struct IndentRulesDef {
83 pub increase: Option<&'static str>,
84 pub decrease: Option<&'static str>,
85 pub indent_next_line: Option<&'static str>,
86 pub dedent_next_line: Option<&'static str>,
87 pub self_close: Option<&'static str>,
88 pub indentation_significant: bool,
95}
96
97pub struct IndentRules {
99 increase: Option<Regex>,
100 decrease: Option<Regex>,
101 indent_next_line: Option<Regex>,
102 dedent_next_line: Option<Regex>,
103 self_close: Option<Regex>,
104 indentation_significant: bool,
105}
106
107impl IndentRules {
108 fn compile(def: &IndentRulesDef) -> Self {
109 Self::compile_parts(
110 def.increase,
111 def.decrease,
112 def.indent_next_line,
113 def.dedent_next_line,
114 def.self_close,
115 def.indentation_significant,
116 )
117 }
118
119 fn compile_parts(
124 increase: Option<&str>,
125 decrease: Option<&str>,
126 indent_next_line: Option<&str>,
127 dedent_next_line: Option<&str>,
128 self_close: Option<&str>,
129 indentation_significant: bool,
130 ) -> Self {
131 let c = |p: Option<&str>| p.and_then(|s| Regex::new(s).ok());
132 Self {
133 indentation_significant,
134 increase: c(increase),
135 decrease: c(decrease),
136 indent_next_line: c(indent_next_line),
137 dedent_next_line: c(dedent_next_line),
138 self_close: c(self_close),
139 }
140 }
141
142 pub fn calculate_indent<F: Fn(usize) -> bool>(
147 &self,
148 buffer: &Buffer,
149 position: usize,
150 tab_size: usize,
151 is_code: F,
152 ) -> usize {
153 let unit = tab_size.max(1);
154
155 let cur = line_bounds(buffer, position);
156 let cur_has_content = first_nonws(buffer, cur.start, position).is_some();
157
158 if self.indentation_significant
169 && !cur_has_content
170 && first_nonws(buffer, position, cur.end).is_none()
171 {
172 return visual_indent(buffer, cur.start, position, tab_size);
173 }
174
175 let reference = if cur_has_content {
179 Some(LineSpan {
180 start: cur.start,
181 end: position,
182 })
183 } else {
184 prev_nonblank_line(buffer, cur.start)
185 };
186
187 let Some(reference) = reference else {
188 return 0;
189 };
190 let base = visual_indent(buffer, reference.start, reference.end, tab_size);
191 let ref_code = code_view(buffer, reference.start, reference.end, &is_code);
192
193 let mut indent = base;
194 let opened = self.increases(&ref_code) || matches(&self.indent_next_line, &ref_code);
195 if opened {
196 indent += unit;
197 } else if matches(&self.dedent_next_line, &ref_code) {
198 indent = indent.saturating_sub(unit);
199 }
200
201 let tail = code_view(buffer, position, cur.end, &is_code);
209 let opener_on_current_line = opened && cur_has_content;
210 if matches(&self.decrease, &tail) && !opener_on_current_line {
211 indent = indent.saturating_sub(unit);
212 }
213
214 indent
215 }
216
217 pub fn calculate_dedent_for_delimiter<F: Fn(usize) -> bool>(
221 &self,
222 buffer: &Buffer,
223 position: usize,
224 ch: char,
225 tab_size: usize,
226 is_code: F,
227 ) -> Option<usize> {
228 let probe = format!("{ch}");
229 if !matches(&self.decrease, &probe) {
230 return None;
231 }
232 let unit = tab_size.max(1);
233 let cur = line_bounds(buffer, position);
234 let reference = prev_nonblank_line(buffer, cur.start)?;
235 let base = visual_indent(buffer, reference.start, reference.end, tab_size);
236 let ref_code = code_view(buffer, reference.start, reference.end, &is_code);
237
238 let mut indent = base;
239 if self.increases(&ref_code) {
240 indent += unit;
241 }
242 Some(indent.saturating_sub(unit))
244 }
245
246 fn increases(&self, code: &str) -> bool {
248 matches(&self.increase, code) && !matches(&self.self_close, code)
249 }
250}
251
252fn matches(re: &Option<Regex>, text: &str) -> bool {
253 re.as_ref().is_some_and(|r| r.is_match(text))
254}
255
256pub fn rules_for_id(id: &str) -> Option<Arc<IndentRules>> {
263 if let Some(rules) = user_rule_for_id(id) {
264 return Some(rules);
265 }
266 let family = family_for_id(id)?;
267 FAMILY_RULES.get(&family).cloned()
268}
269
270pub fn user_rule_for_id(id: &str) -> Option<Arc<IndentRules>> {
281 USER_RULES.read().unwrap().get(id).cloned()
282}
283
284pub fn rules_for_syntax_name(name: &str) -> Option<Arc<IndentRules>> {
289 let lower = name.to_ascii_lowercase();
290 let id = match lower.as_str() {
291 "c++" => "cpp",
292 "c#" => "csharp",
293 n if n.contains("typescript") => "typescript",
294 n if n.contains("javascript") => "javascript",
295 n if n.contains("bash") || n.contains("shell") => "bash",
297 other => other,
298 };
299 rules_for_id(id)
300}
301
302fn family_for_id(id: &str) -> Option<Family> {
305 let f = match id {
306 "rust" | "c" | "cpp" | "c++" | "csharp" | "c_sharp" | "java" | "go" | "javascript"
307 | "typescript" | "typescriptreact" | "javascriptreact" | "php" | "swift" | "kotlin"
308 | "dart" | "scala" | "json" | "jsonc" | "css" | "scss" | "less" => Family::CurlyBrace,
309 "python" => Family::Python,
310 "ruby" => Family::RubyLike,
311 "lua" => Family::LuaLike,
312 "bash" | "sh" | "shell" | "shellscript" => Family::BashLike,
313 "fish" => Family::FishLike,
314 "pascal" => Family::PascalLike,
315 "smali" => Family::SmaliLike,
316 _ => return None,
317 };
318 Some(f)
319}
320
321fn def_for_family(family: Family) -> &'static IndentRulesDef {
324 match family {
325 Family::CurlyBrace => &CURLY_BRACE,
326 Family::Python => &PYTHON,
327 Family::RubyLike => &RUBY_LIKE,
328 Family::LuaLike => &LUA_LIKE,
329 Family::BashLike => &BASH_LIKE,
330 Family::FishLike => &FISH_LIKE,
331 Family::PascalLike => &PASCAL_LIKE,
332 Family::SmaliLike => &SMALI_LIKE,
333 }
334}
335
336static FAMILY_RULES: Lazy<HashMap<Family, Arc<IndentRules>>> = Lazy::new(|| {
338 let mut m = HashMap::new();
339 for family in [
340 Family::CurlyBrace,
341 Family::Python,
342 Family::RubyLike,
343 Family::LuaLike,
344 Family::BashLike,
345 Family::FishLike,
346 Family::PascalLike,
347 Family::SmaliLike,
348 ] {
349 m.insert(
350 family,
351 Arc::new(IndentRules::compile(def_for_family(family))),
352 );
353 }
354 m
355});
356
357static USER_RULES: Lazy<RwLock<HashMap<String, Arc<IndentRules>>>> =
361 Lazy::new(|| RwLock::new(HashMap::new()));
362
363pub fn clear_user_rules() {
366 USER_RULES.write().unwrap().clear();
367}
368
369pub fn set_user_rule(
378 id: &str,
379 increase: Option<&str>,
380 decrease: Option<&str>,
381 indent_next_line: Option<&str>,
382 dedent_next_line: Option<&str>,
383 self_close: Option<&str>,
384) {
385 let base = family_for_id(id).map(def_for_family);
388 let rules = IndentRules::compile_parts(
389 increase.or(base.and_then(|d| d.increase)),
390 decrease.or(base.and_then(|d| d.decrease)),
391 indent_next_line.or(base.and_then(|d| d.indent_next_line)),
392 dedent_next_line.or(base.and_then(|d| d.dedent_next_line)),
393 self_close.or(base.and_then(|d| d.self_close)),
394 base.map(|d| d.indentation_significant).unwrap_or(false),
395 );
396 USER_RULES
397 .write()
398 .unwrap()
399 .insert(id.to_string(), Arc::new(rules));
400}
401
402const CURLY_BRACE: IndentRulesDef = IndentRulesDef {
403 increase: Some(r"[\{\[\(]\s*$"),
406 decrease: Some(r"^\s*[\}\]\)]"),
408 indent_next_line: Some(r"^\s*((if|for|while)\b.*\)|else)\s*$"),
410 dedent_next_line: None,
411 self_close: None,
412 indentation_significant: false,
413};
414
415const SMALI_LIKE: IndentRulesDef = IndentRulesDef {
416 increase: Some(
417 r"(?x)
418 (^\s*\.
419 (?:method|annotation|subannotation|packed-switch|sparse-switch|array-data|param|parameter)
420 \b
421 )
422 |
423 ([\{\[\(]\s*$)
424 ",
425 ),
426 decrease: Some(
427 r"(?x)^\s*
428 (?:
429 \.end\s+
430 (?:method|annotation|subannotation|packed-switch|sparse-switch|array-data|param|parameter)\b
431 |
432 [\}\]\)]
433 )
434 ",
435 ),
436 indent_next_line: None,
437 dedent_next_line: None,
438 self_close: Some(
439 r"(?x)^\s*\.
440 (?:method|annotation|subannotation|packed-switch|sparse-switch|array-data|param|parameter)
441 \b.*\s\.end\s+
442 (?:method|annotation|subannotation|packed-switch|sparse-switch|array-data|param|parameter)\b
443 ",
444 ),
445 indentation_significant: false,
446};
447
448const PYTHON: IndentRulesDef = IndentRulesDef {
449 increase: Some(r":\s*$"),
450 decrease: Some(r"^\s*(elif|else|except|finally|case)\b"),
452 indent_next_line: None,
453 dedent_next_line: Some(r"^\s*(return|pass|raise|break|continue)\b"),
454 self_close: None,
455 indentation_significant: true,
456};
457
458const RUBY_LIKE: IndentRulesDef = IndentRulesDef {
459 increase: Some(
461 r"(^\s*(if|unless|while|until|for|begin|def|class|module|case|else|elsif|when|in|rescue|ensure)\b)|(\bdo(\s*\|[^|]*\|)?\s*$)",
462 ),
463 decrease: Some(r"^\s*(end|else|elsif|when|in|rescue|ensure)\b"),
465 indent_next_line: None,
466 dedent_next_line: None,
467 self_close: Some(r"\bend\b"),
469 indentation_significant: false,
470};
471
472const LUA_LIKE: IndentRulesDef = IndentRulesDef {
473 increase: Some(
474 r"(^\s*((local\s+)?function|if|elseif|else|for|while|repeat)\b)|(\b(do|then)\s*$)",
475 ),
476 decrease: Some(r"^\s*(end|else|elseif|until)\b"),
477 indent_next_line: None,
478 dedent_next_line: None,
479 self_close: Some(r"\bend\b"),
480 indentation_significant: false,
481};
482
483const BASH_LIKE: IndentRulesDef = IndentRulesDef {
484 increase: Some(r"(\b(then|do)\s*$)|(^\s*case\b.*\bin\s*$)|(\{\s*$)"),
489 decrease: Some(r"^\s*(fi|done|esac|else|elif|\})"),
490 indent_next_line: None,
491 dedent_next_line: None,
492 self_close: None,
493 indentation_significant: false,
494};
495
496const FISH_LIKE: IndentRulesDef = IndentRulesDef {
497 increase: Some(r"^\s*(if|else|for|while|begin|function|switch|case)\b"),
498 decrease: Some(r"^\s*(end|else|case)\b"),
499 indent_next_line: None,
500 dedent_next_line: None,
501 self_close: Some(r"\bend\b"),
502 indentation_significant: false,
503};
504
505const PASCAL_LIKE: IndentRulesDef = IndentRulesDef {
506 increase: Some(r"(^\s*(begin|case|record|try|repeat|asm)\b)|(\b(begin|of)\s*$)"),
507 decrease: Some(r"^\s*(end|until|except|finally)\b"),
508 indent_next_line: None,
509 dedent_next_line: None,
510 self_close: Some(r"\bend\b"),
511 indentation_significant: false,
512};
513
514#[derive(Clone, Copy)]
520struct LineSpan {
521 start: usize,
522 end: usize,
523}
524
525fn byte_at(buffer: &Buffer, pos: usize) -> Option<u8> {
526 if pos >= buffer.len() {
527 return None;
528 }
529 buffer.slice_bytes(pos..pos + 1).first().copied()
530}
531
532fn line_bounds(buffer: &Buffer, position: usize) -> LineSpan {
535 let mut start = position;
536 while start > 0 && byte_at(buffer, start - 1) != Some(b'\n') {
537 start -= 1;
538 }
539 let mut end = position;
540 while end < buffer.len() && byte_at(buffer, end) != Some(b'\n') {
541 end += 1;
542 }
543 LineSpan { start, end }
544}
545
546fn first_nonws(buffer: &Buffer, start: usize, end: usize) -> Option<usize> {
548 let mut p = start;
549 while p < end {
550 match byte_at(buffer, p) {
551 Some(b' ') | Some(b'\t') | Some(b'\r') => p += 1,
552 Some(_) => return Some(p),
553 None => return None,
554 }
555 }
556 None
557}
558
559fn prev_nonblank_line(buffer: &Buffer, line_start: usize) -> Option<LineSpan> {
561 if line_start == 0 {
562 return None;
563 }
564 let mut pos = line_start - 1; loop {
566 let span = line_bounds(buffer, pos);
567 if first_nonws(buffer, span.start, span.end).is_some() {
568 return Some(span);
569 }
570 if span.start == 0 {
571 return None;
572 }
573 pos = span.start - 1;
574 }
575}
576
577fn visual_indent(buffer: &Buffer, start: usize, end: usize, tab_size: usize) -> usize {
579 let mut indent = 0;
580 let mut p = start;
581 while p < end {
582 match byte_at(buffer, p) {
583 Some(b' ') => indent += 1,
584 Some(b'\t') => indent += tab_size,
585 _ => break,
586 }
587 p += 1;
588 }
589 indent
590}
591
592fn code_view<F: Fn(usize) -> bool>(
595 buffer: &Buffer,
596 start: usize,
597 end: usize,
598 is_code: &F,
599) -> String {
600 let bytes = buffer.slice_bytes(start..end);
601 let mut out = String::with_capacity(bytes.len());
602 for (i, &b) in bytes.iter().enumerate() {
603 if b == b'\r' || b == b'\n' {
604 continue;
605 }
606 if is_code(start + i) {
608 out.push(b as char);
609 } else {
610 out.push(' ');
611 }
612 }
613 out
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use crate::model::filesystem::NoopFileSystem;
620 use std::sync::Arc;
621
622 fn buf(content: &str) -> Buffer {
623 let fs = Arc::new(NoopFileSystem);
624 let mut b = Buffer::empty(fs);
625 b.insert(0, content);
626 b
627 }
628
629 fn indent(id: &str, content: &str, tab: usize) -> usize {
631 rules_for_id(id)
632 .unwrap()
633 .calculate_indent(&buf(content), content.len(), tab, |_| true)
634 }
635
636 fn indent_masked(id: &str, content: &str, tab: usize, masked: &[(usize, usize)]) -> usize {
639 let b = buf(content);
640 let is_code = |byte: usize| !masked.iter().any(|&(s, e)| byte >= s && byte < e);
641 rules_for_id(id)
642 .unwrap()
643 .calculate_indent(&b, content.len(), tab, is_code)
644 }
645
646 #[test]
649 fn curly_indents_after_open_brace() {
650 assert_eq!(indent("rust", "fn main() {\n", 4), 4);
651 assert_eq!(indent("typescript", "function f() {\n", 4), 4);
652 }
653
654 #[test]
655 fn curly_no_indent_after_balanced_line() {
656 assert_eq!(indent("rust", "let x = 1;\n", 4), 0);
657 assert_eq!(indent("rust", "fn x() { return 1; }\n", 4), 0);
659 }
660
661 #[test]
662 fn curly_dedents_before_close_brace() {
663 let content = "fn main() {\n }";
665 let pos = content.len() - 1; let b = buf(content);
667 let got = rules_for_id("rust")
668 .unwrap()
669 .calculate_indent(&b, pos, 4, |_| true);
670 assert_eq!(got, 0);
671 }
672
673 #[test]
674 fn curly_braceless_if_indents_next_line_only() {
675 assert_eq!(indent("c", "if (x)\n", 4), 4);
676 }
677
678 #[test]
679 fn curly_dedent_for_typed_brace() {
680 let content = "fn main() {\n body\n";
681 let dedent = rules_for_id("rust")
682 .unwrap()
683 .calculate_dedent_for_delimiter(&buf(content), content.len(), '}', 4, |_| true);
684 assert_eq!(dedent, Some(0));
685 }
686
687 #[test]
690 fn no_indent_for_brace_in_string() {
691 let content = "let x = \"{\";\n";
693 let open = content.find('{').unwrap();
694 let masked = [(content.find('"').unwrap(), open + 2)];
696 assert_eq!(indent_masked("rust", content, 4, &masked), 0);
697 assert_eq!(indent("rust", content, 4), 0); }
700
701 #[test]
702 fn no_indent_for_trailing_brace_in_comment() {
703 let content = "foo() // {\n";
705 let cstart = content.find("//").unwrap();
706 let masked = [(cstart, content.len())];
707 assert_eq!(indent_masked("rust", content, 4, &masked), 0);
708 }
709
710 #[test]
711 fn brace_in_comment_does_not_defeat_real_open() {
712 let content = "if (x) { // start {\n";
714 let cstart = content.find("//").unwrap();
715 let masked = [(cstart, content.len())];
716 assert_eq!(indent_masked("rust", content, 4, &masked), 4);
718 }
719
720 #[test]
725 fn python_indents_after_colon() {
726 assert_eq!(indent("python", "def foo():", 4), 4);
727 assert_eq!(indent("python", "if x:", 4), 4);
728 }
729
730 #[test]
731 fn python_dedents_after_return() {
732 let content = "def foo():\n return 1";
733 assert_eq!(indent("python", content, 4), 0);
734 }
735
736 #[test]
737 fn python_keeps_indent_inside_body() {
738 let content = "def foo():\n x = 1";
739 assert_eq!(indent("python", content, 4), 4);
740 }
741
742 #[test]
743 fn python_blank_line_keeps_manual_dedent() {
744 let content = "if x:\n foo()\n"; assert_eq!(indent("python", content, 4), 0);
750 }
751
752 #[test]
753 fn python_blank_line_maintains_current_column() {
754 let content = "if x:\n foo()\n "; assert_eq!(indent("python", content, 4), 4);
758 }
759
760 #[test]
761 fn curly_blank_line_rederives_not_preserved() {
762 assert_eq!(indent("rust", "fn f() {\n", 4), 4);
766 }
767
768 #[test]
769 fn python_colon_in_string_does_not_indent() {
770 let content = "s = \"key:\"";
773 let q1 = content.find('"').unwrap();
774 let q2 = content.rfind('"').unwrap();
775 let masked = [(q1, q2 + 1)];
776 assert_eq!(indent_masked("python", content, 4, &masked), 0);
777 }
778
779 #[test]
782 fn ruby_indents_after_def_and_do() {
783 assert_eq!(indent("ruby", "def foo\n", 2), 2);
784 assert_eq!(indent("ruby", "[1,2].each do |n|\n", 2), 2);
785 }
786
787 #[test]
788 fn ruby_one_liner_with_end_does_not_indent() {
789 assert_eq!(indent("ruby", "def foo; end\n", 2), 0);
790 assert_eq!(indent("ruby", "if x then y end\n", 2), 0);
791 }
792
793 #[test]
794 fn ruby_end_in_string_does_not_dedent_or_break() {
795 let content = "x = 1\ns = \"end\"\n";
797 let q1 = content.rfind('"').unwrap();
798 let qs = content[..q1].rfind('"').unwrap();
800 let masked = [(qs, q1 + 1)];
801 assert_eq!(indent_masked("ruby", content, 2, &masked), 0);
803 }
804
805 #[test]
806 fn ruby_midblock_else_reindents_body() {
807 let content = "if x\n a\nelse\n";
809 assert_eq!(indent("ruby", content, 2), 2);
810 }
811
812 #[test]
815 fn lua_indents_after_block_openers() {
816 assert_eq!(indent("lua", "function f()\n", 4), 4);
817 assert_eq!(indent("lua", "if x then\n", 4), 4);
818 assert_eq!(indent("lua", "for i = 1, n do\n", 4), 4);
819 }
820
821 #[test]
822 fn lua_one_liner_with_end_does_not_indent() {
823 assert_eq!(indent("lua", "function f() end\n", 4), 0);
824 }
825
826 #[test]
829 fn bash_indents_after_then_do_case() {
830 assert_eq!(indent("bash", "if true; then\n", 4), 4);
831 assert_eq!(indent("bash", "for x in a b; do\n", 4), 4);
832 assert_eq!(indent("bash", "case $x in\n", 4), 4);
833 }
834
835 #[test]
836 fn bash_resolves_from_syntect_name() {
837 assert!(rules_for_syntax_name("Bourne Again Shell (bash)").is_some());
839 }
840
841 #[test]
842 fn fish_indents_after_block_openers() {
843 assert_eq!(indent("fish", "if test -n \"$name\"\n", 4), 4);
844 assert_eq!(indent("fish", "for item in $items\n", 4), 4);
845 assert_eq!(
846 indent("fish", "function greet --argument-names name\n", 4),
847 4
848 );
849 assert_eq!(indent("fish", "switch $name\n", 4), 4);
850 assert_eq!(indent("fish", "case fresh\n", 4), 4);
851 }
852
853 #[test]
854 fn fish_one_liner_with_end_does_not_indent() {
855 assert_eq!(
856 indent("fish", "if test -n \"$name\"; echo $name; end\n", 4),
857 0
858 );
859 }
860
861 #[test]
864 fn pascal_indents_after_begin() {
865 assert_eq!(indent("pascal", "begin\n", 4), 4);
866 assert_eq!(indent("pascal", "if x then begin\n", 4), 4);
867 }
868
869 #[test]
870 fn pascal_one_liner_with_end_does_not_indent() {
871 assert_eq!(indent("pascal", "begin end;\n", 4), 0);
872 }
873
874 #[test]
877 fn smali_indents_after_method_directive() {
878 assert_eq!(
879 indent(
880 "smali",
881 ".method public onCreate(Landroid/os/Bundle;)V\n",
882 4
883 ),
884 4
885 );
886 }
887
888 #[test]
889 fn smali_dedents_before_end_directive() {
890 let content = ".method public main()V\n .end method";
891 let pos = content.find(".end method").unwrap();
892 let got = rules_for_id("smali")
893 .unwrap()
894 .calculate_indent(&buf(content), pos, 4, |_| true);
895 assert_eq!(got, 0);
896 }
897
898 #[test]
899 fn smali_plain_field_does_not_indent() {
900 assert_eq!(indent("smali", ".field public static count:I = 0\n", 4), 0);
901 }
902
903 #[test]
906 fn unknown_language_has_no_rules() {
907 assert!(rules_for_id("brainfuck").is_none());
908 }
909
910 #[test]
911 fn families_compile() {
912 assert!(rules_for_id("rust").unwrap().increase.is_some());
914 assert!(rules_for_id("python").unwrap().dedent_next_line.is_some());
915 assert!(rules_for_id("ruby").unwrap().self_close.is_some());
916 }
917
918 #[test]
921 fn user_overrides_register_and_merge() {
922 clear_user_rules();
923
924 set_user_rule(
927 "zz_newlang",
928 Some(r":\s*$"),
929 Some(r"^\s*end\b"),
930 None,
931 None,
932 None,
933 );
934 let r = rules_for_id("zz_newlang").expect("user rule registered");
935 assert_eq!(r.calculate_indent(&buf("foo:"), 4, 4, |_| true), 4);
936
937 set_user_rule("kotlin", Some(r"=>\s*$"), None, None, None, None);
940 let k = rules_for_id("kotlin").expect("kotlin via override");
941 assert!(
942 k.decrease.is_some(),
943 "decrease inherited from CurlyBrace family"
944 );
945 let c = "val f = x =>";
946 assert_eq!(k.calculate_indent(&buf(c), c.len(), 4, |_| true), 4);
947
948 clear_user_rules();
949 assert!(rules_for_id("zz_newlang").is_none(), "override cleared");
950 assert!(rules_for_id("kotlin").is_some());
952 }
953}
954
955#[cfg(all(test, feature = "tree-sitter"))]
977mod parity {
978 use super::*;
979 use crate::model::filesystem::NoopFileSystem;
980 use crate::primitives::indent::IndentCalculator;
981 use fresh_languages::Language;
982 use std::sync::Arc;
983
984 fn buf(content: &str) -> Buffer {
985 let fs = Arc::new(NoopFileSystem);
986 let mut b = Buffer::empty(fs);
987 b.insert(0, content);
988 b
989 }
990
991 #[test]
992 fn rules_match_tree_sitter_on_corpus() {
993 let cases: &[(Language, &str, &str)] = &[
1002 (Language::TypeScript, "typescript", "function f() {"),
1003 (Language::TypeScript, "typescript", "class A {"),
1004 (Language::TypeScript, "typescript", "let x = 1;"),
1005 (Language::Go, "go", "func main() {"),
1006 (Language::JavaScript, "javascript", "function f() {"),
1007 ];
1008
1009 let tab = 4;
1010 let mut mismatches = Vec::new();
1011 let mut compared = 0;
1012 for (lang, id, code) in cases {
1013 let ts = {
1014 let mut calc = IndentCalculator::new();
1015 calc.calculate_indent(&buf(code), code.len(), lang, tab)
1016 };
1017 let Some(ts) = ts else { continue }; compared += 1;
1019 let rules = rules_for_id(id)
1020 .unwrap_or_else(|| panic!("no rules for {id}"))
1021 .calculate_indent(&buf(code), code.len(), tab, |_| true);
1022 if ts != rules {
1023 mismatches.push(format!(
1024 " {id}: code={code:?} tree-sitter={ts} rules={rules}"
1025 ));
1026 }
1027 }
1028
1029 assert!(
1030 mismatches.is_empty(),
1031 "rules tier diverged from tree-sitter on {}/{} compared cases:\n{}",
1032 mismatches.len(),
1033 compared,
1034 mismatches.join("\n")
1035 );
1036 assert!(
1039 compared >= 4,
1040 "too few comparable cases ({compared}); guard is vacuous"
1041 );
1042 }
1043}