languages: Fix indentation for if/else statements in C/C++ without braces (#41670)

Mayank Verma created

Closes #41179

Release Notes:

- Fixed indentation for if/else statements in C/C++ without braces

Change summary

crates/languages/Cargo.toml          |   1 
crates/languages/src/c.rs            | 231 +++++++++++++++++++++++++-
crates/languages/src/c/config.toml   |   2 
crates/languages/src/cpp.rs          | 254 ++++++++++++++++++++++++++++++
crates/languages/src/cpp/config.toml |   2 
crates/languages/src/lib.rs          |   1 
6 files changed, 477 insertions(+), 14 deletions(-)

Detailed changes

crates/languages/Cargo.toml 🔗

@@ -98,6 +98,7 @@ text.workspace = true
 theme = { workspace = true, features = ["test-support"] }
 tree-sitter-bash.workspace = true
 tree-sitter-c.workspace = true
+tree-sitter-cpp.workspace = true
 tree-sitter-css.workspace = true
 tree-sitter-go.workspace = true
 tree-sitter-python.workspace = true

crates/languages/src/c.rs 🔗

@@ -395,10 +395,10 @@ mod tests {
     use language::{AutoindentMode, Buffer};
     use settings::SettingsStore;
     use std::num::NonZeroU32;
+    use unindent::Unindent;
 
     #[gpui::test]
-    async fn test_c_autoindent(cx: &mut TestAppContext) {
-        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
+    async fn test_c_autoindent_basic(cx: &mut TestAppContext) {
         cx.update(|cx| {
             let test_settings = SettingsStore::test(cx);
             cx.set_global(test_settings);
@@ -413,23 +413,230 @@ mod tests {
         cx.new(|cx| {
             let mut buffer = Buffer::local("", cx).with_language(language, cx);
 
-            // empty function
             buffer.edit([(0..0, "int main() {}")], None, cx);
 
-            // indent inside braces
             let ix = buffer.len() - 1;
             buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
-            assert_eq!(buffer.text(), "int main() {\n  \n}");
+            assert_eq!(
+                buffer.text(),
+                "int main() {\n  \n}",
+                "content inside braces should be indented"
+            );
 
-            // indent body of single-statement if statement
-            let ix = buffer.len() - 2;
-            buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
-            assert_eq!(buffer.text(), "int main() {\n  if (a)\n    b;\n}");
+            buffer
+        });
+    }
 
-            // indent inside field expression
-            let ix = buffer.len() - 3;
+    #[gpui::test]
+    async fn test_c_autoindent_if_else(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings(cx, |s| {
+                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+        let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
+
+        cx.new(|cx| {
+            let mut buffer = Buffer::local("", cx).with_language(language, cx);
+
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a)
+                    b;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    b;
+                }
+                "#
+                .unindent(),
+                "body of if-statement without braces should be indented"
+            );
+
+            let ix = buffer.len() - 4;
             buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
-            assert_eq!(buffer.text(), "int main() {\n  if (a)\n    b\n      .c;\n}");
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    b
+                      .c;
+                }
+                "#
+                .unindent(),
+                "field expression (.c) should be indented further than the statement body"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a) a++;
+                    else b++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a) a++;
+                  else b++;
+                }
+                "#
+                .unindent(),
+                "single-line if/else without braces should align at the same level"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a)
+                    b++;
+                    else
+                    c++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    b++;
+                  else
+                    c++;
+                }
+                "#
+                .unindent(),
+                "multi-line if/else without braces should indent statement bodies"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a)
+                    if (b)
+                    c++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    if (b)
+                      c++;
+                }
+                "#
+                .unindent(),
+                "nested if statements without braces should indent properly"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a)
+                    b++;
+                    else if (c)
+                    d++;
+                    else
+                    f++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    b++;
+                  else if (c)
+                    d++;
+                  else
+                    f++;
+                }
+                "#
+                .unindent(),
+                "else-if chains should align all conditions at same level with indented bodies"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a) {
+                    b++;
+                    } else
+                    c++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a) {
+                    b++;
+                  } else
+                    c++;
+                }
+                "#
+                .unindent(),
+                "mixed braces should indent properly"
+            );
 
             buffer
         });

crates/languages/src/c/config.toml 🔗

@@ -4,7 +4,7 @@ path_suffixes = ["c"]
 line_comments = ["// "]
 decrease_indent_patterns = [
   { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] },
-  { pattern = "^\\s*else\\s*$", valid_after = ["if"] }
+  { pattern = "^\\s*else\\b", valid_after = ["if"] }
 ]
 autoclose_before = ";:.,=}])>"
 brackets = [

crates/languages/src/cpp.rs 🔗

@@ -0,0 +1,254 @@
+#[cfg(test)]
+mod tests {
+    use gpui::{AppContext as _, BorrowAppContext, TestAppContext};
+    use language::{AutoindentMode, Buffer};
+    use settings::SettingsStore;
+    use std::num::NonZeroU32;
+    use unindent::Unindent;
+
+    #[gpui::test]
+    async fn test_cpp_autoindent_basic(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings(cx, |s| {
+                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+        let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
+
+        cx.new(|cx| {
+            let mut buffer = Buffer::local("", cx).with_language(language, cx);
+
+            buffer.edit([(0..0, "int main() {}")], None, cx);
+
+            let ix = buffer.len() - 1;
+            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(
+                buffer.text(),
+                "int main() {\n  \n}",
+                "content inside braces should be indented"
+            );
+
+            buffer
+        });
+    }
+
+    #[gpui::test]
+    async fn test_cpp_autoindent_if_else(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings(cx, |s| {
+                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+        let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
+
+        cx.new(|cx| {
+            let mut buffer = Buffer::local("", cx).with_language(language, cx);
+
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a)
+                    b;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    b;
+                }
+                "#
+                .unindent(),
+                "body of if-statement without braces should be indented"
+            );
+
+            let ix = buffer.len() - 4;
+            buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    b
+                      .c;
+                }
+                "#
+                .unindent(),
+                "field expression (.c) should be indented further than the statement body"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a) a++;
+                    else b++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a) a++;
+                  else b++;
+                }
+                "#
+                .unindent(),
+                "single-line if/else without braces should align at the same level"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a)
+                    b++;
+                    else
+                    c++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    b++;
+                  else
+                    c++;
+                }
+                "#
+                .unindent(),
+                "multi-line if/else without braces should indent statement bodies"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a)
+                    if (b)
+                    c++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    if (b)
+                      c++;
+                }
+                "#
+                .unindent(),
+                "nested if statements without braces should indent properly"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a)
+                    b++;
+                    else if (c)
+                    d++;
+                    else
+                    f++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a)
+                    b++;
+                  else if (c)
+                    d++;
+                  else
+                    f++;
+                }
+                "#
+                .unindent(),
+                "else-if chains should align all conditions at same level with indented bodies"
+            );
+
+            buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
+            buffer.edit(
+                [(
+                    0..0,
+                    r#"
+                    int main() {
+                    if (a) {
+                    b++;
+                    } else
+                    c++;
+                    }
+                    "#
+                    .unindent(),
+                )],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                r#"
+                int main() {
+                  if (a) {
+                    b++;
+                  } else
+                    c++;
+                }
+                "#
+                .unindent(),
+                "mixed braces should indent properly"
+            );
+
+            buffer
+        });
+    }
+}

crates/languages/src/cpp/config.toml 🔗

@@ -4,7 +4,7 @@ path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl
 line_comments = ["// ", "/// ", "//! "]
 decrease_indent_patterns = [
   { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] },
-  { pattern = "^\\s*else\\s*$", valid_after = ["if"] }
+  { pattern = "^\\s*else\\b", valid_after = ["if"] }
 ]
 autoclose_before = ";:.,=}])>"
 brackets = [