Cargo.lock 🔗
@@ -4979,6 +4979,7 @@ dependencies = [
"text",
"theme",
"time",
+ "tree-sitter-bash",
"tree-sitter-html",
"tree-sitter-python",
"tree-sitter-rust",
Smit Barmase created
Closes #34390
This PR fixes several Bash indentation issues:
- Adding indentation or comment using multi cursors no longer breaks
relative indentation
- Adding newline now places the cursor at the correct indent
- Typing a valid keyword triggers context-aware auto outdent
It also adds tests for all of them.
Release Notes:
- Fixed various issues with handling indentation in Bash.
Cargo.lock | 1
crates/editor/Cargo.toml | 1
crates/editor/src/editor_tests.rs | 429 +++++++++++++++++++++++++++++
crates/languages/src/bash/config.toml | 31 +
crates/languages/src/bash/indents.scm | 22
5 files changed, 459 insertions(+), 25 deletions(-)
@@ -4979,6 +4979,7 @@ dependencies = [
"text",
"theme",
"time",
+ "tree-sitter-bash",
"tree-sitter-html",
"tree-sitter-python",
"tree-sitter-rust",
@@ -110,6 +110,7 @@ tree-sitter-html.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true
tree-sitter-yaml.workspace = true
+tree-sitter-bash.workspace = true
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
@@ -22585,6 +22585,435 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
"});
}
+#[gpui::test]
+async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ // test cursor move to start of each line on tab
+ // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
+ cx.set_state(indoc! {"
+ function main() {
+ ˇ for item in $items; do
+ ˇ while [ -n \"$item\" ]; do
+ ˇ if [ \"$value\" -gt 10 ]; then
+ ˇ continue
+ ˇ elif [ \"$value\" -lt 0 ]; then
+ ˇ break
+ ˇ else
+ ˇ echo \"$item\"
+ ˇ fi
+ ˇ done
+ ˇ done
+ ˇ}
+ "});
+ cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.assert_editor_state(indoc! {"
+ function main() {
+ ˇfor item in $items; do
+ ˇwhile [ -n \"$item\" ]; do
+ ˇif [ \"$value\" -gt 10 ]; then
+ ˇcontinue
+ ˇelif [ \"$value\" -lt 0 ]; then
+ ˇbreak
+ ˇelse
+ ˇecho \"$item\"
+ ˇfi
+ ˇdone
+ ˇdone
+ ˇ}
+ "});
+ // test relative indent is preserved when tab
+ cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.assert_editor_state(indoc! {"
+ function main() {
+ ˇfor item in $items; do
+ ˇwhile [ -n \"$item\" ]; do
+ ˇif [ \"$value\" -gt 10 ]; then
+ ˇcontinue
+ ˇelif [ \"$value\" -lt 0 ]; then
+ ˇbreak
+ ˇelse
+ ˇecho \"$item\"
+ ˇfi
+ ˇdone
+ ˇdone
+ ˇ}
+ "});
+
+ // test cursor move to start of each line on tab
+ // for `case` statement with patterns
+ cx.set_state(indoc! {"
+ function handle() {
+ ˇ case \"$1\" in
+ ˇ start)
+ ˇ echo \"a\"
+ ˇ ;;
+ ˇ stop)
+ ˇ echo \"b\"
+ ˇ ;;
+ ˇ *)
+ ˇ echo \"c\"
+ ˇ ;;
+ ˇ esac
+ ˇ}
+ "});
+ cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.assert_editor_state(indoc! {"
+ function handle() {
+ ˇcase \"$1\" in
+ ˇstart)
+ ˇecho \"a\"
+ ˇ;;
+ ˇstop)
+ ˇecho \"b\"
+ ˇ;;
+ ˇ*)
+ ˇecho \"c\"
+ ˇ;;
+ ˇesac
+ ˇ}
+ "});
+}
+
+#[gpui::test]
+async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ // test indents on comment insert
+ cx.set_state(indoc! {"
+ function main() {
+ ˇ for item in $items; do
+ ˇ while [ -n \"$item\" ]; do
+ ˇ if [ \"$value\" -gt 10 ]; then
+ ˇ continue
+ ˇ elif [ \"$value\" -lt 0 ]; then
+ ˇ break
+ ˇ else
+ ˇ echo \"$item\"
+ ˇ fi
+ ˇ done
+ ˇ done
+ ˇ}
+ "});
+ cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
+ cx.assert_editor_state(indoc! {"
+ function main() {
+ #ˇ for item in $items; do
+ #ˇ while [ -n \"$item\" ]; do
+ #ˇ if [ \"$value\" -gt 10 ]; then
+ #ˇ continue
+ #ˇ elif [ \"$value\" -lt 0 ]; then
+ #ˇ break
+ #ˇ else
+ #ˇ echo \"$item\"
+ #ˇ fi
+ #ˇ done
+ #ˇ done
+ #ˇ}
+ "});
+}
+
+#[gpui::test]
+async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ // test `else` auto outdents when typed inside `if` block
+ cx.set_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ echo \"foo bar\"
+ ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("else", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ echo \"foo bar\"
+ elseˇ
+ "});
+
+ // test `elif` auto outdents when typed inside `if` block
+ cx.set_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ echo \"foo bar\"
+ ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("elif", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ echo \"foo bar\"
+ elifˇ
+ "});
+
+ // test `fi` auto outdents when typed inside `else` block
+ cx.set_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ echo \"foo bar\"
+ else
+ echo \"bar baz\"
+ ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("fi", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ echo \"foo bar\"
+ else
+ echo \"bar baz\"
+ fiˇ
+ "});
+
+ // test `done` auto outdents when typed inside `while` block
+ cx.set_state(indoc! {"
+ while read line; do
+ echo \"$line\"
+ ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("done", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ while read line; do
+ echo \"$line\"
+ doneˇ
+ "});
+
+ // test `done` auto outdents when typed inside `for` block
+ cx.set_state(indoc! {"
+ for file in *.txt; do
+ cat \"$file\"
+ ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("done", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ for file in *.txt; do
+ cat \"$file\"
+ doneˇ
+ "});
+
+ // test `esac` auto outdents when typed inside `case` block
+ cx.set_state(indoc! {"
+ case \"$1\" in
+ start)
+ echo \"foo bar\"
+ ;;
+ stop)
+ echo \"bar baz\"
+ ;;
+ ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("esac", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ case \"$1\" in
+ start)
+ echo \"foo bar\"
+ ;;
+ stop)
+ echo \"bar baz\"
+ ;;
+ esacˇ
+ "});
+
+ // test `*)` auto outdents when typed inside `case` block
+ cx.set_state(indoc! {"
+ case \"$1\" in
+ start)
+ echo \"foo bar\"
+ ;;
+ ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("*)", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ case \"$1\" in
+ start)
+ echo \"foo bar\"
+ ;;
+ *)ˇ
+ "});
+
+ // test `fi` outdents to correct level with nested if blocks
+ cx.set_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ echo \"outer if\"
+ if [ \"$2\" = \"debug\" ]; then
+ echo \"inner if\"
+ ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("fi", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ echo \"outer if\"
+ if [ \"$2\" = \"debug\" ]; then
+ echo \"inner if\"
+ fiˇ
+ "});
+}
+
+#[gpui::test]
+async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.extend_comment_on_newline = Some(false);
+ });
+ let mut cx = EditorTestContext::new(cx).await;
+ let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ // test correct indent after newline on comment
+ cx.set_state(indoc! {"
+ # COMMENT:ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ # COMMENT:
+ ˇ
+ "});
+
+ // test correct indent after newline after `then`
+ cx.set_state(indoc! {"
+
+ if [ \"$1\" = \"test\" ]; thenˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+
+ if [ \"$1\" = \"test\" ]; then
+ ˇ
+ "});
+
+ // test correct indent after newline after `else`
+ cx.set_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ elseˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ else
+ ˇ
+ "});
+
+ // test correct indent after newline after `elif`
+ cx.set_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ elifˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ if [ \"$1\" = \"test\" ]; then
+ elif
+ ˇ
+ "});
+
+ // test correct indent after newline after `do`
+ cx.set_state(indoc! {"
+ for file in *.txt; doˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ for file in *.txt; do
+ ˇ
+ "});
+
+ // test correct indent after newline after case pattern
+ cx.set_state(indoc! {"
+ case \"$1\" in
+ start)ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ case \"$1\" in
+ start)
+ ˇ
+ "});
+
+ // test correct indent after newline after case pattern
+ cx.set_state(indoc! {"
+ case \"$1\" in
+ start)
+ ;;
+ *)ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ case \"$1\" in
+ start)
+ ;;
+ *)
+ ˇ
+ "});
+
+ // test correct indent after newline after function opening brace
+ cx.set_state(indoc! {"
+ function test() {ˇ}
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ function test() {
+ ˇ
+ }
+ "});
+
+ // test no extra indent after semicolon on same line
+ cx.set_state(indoc! {"
+ echo \"test\";ˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.newline(&Newline, window, cx);
+ });
+ cx.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ echo \"test\";
+ ˇ
+ "});
+}
+
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point
@@ -18,17 +18,20 @@ brackets = [
{ start = "in", end = "esac", close = false, newline = true, not_in = ["comment", "string"] },
]
-### WARN: the following is not working when you insert an `elif` just before an else
-### example: (^ is cursor after hitting enter)
-### ```
-### if true; then
-### foo
-### elif
-### ^
-### else
-### bar
-### fi
-### ```
-increase_indent_pattern = "(^|\\s+|;)(do|then|in|else|elif)\\b.*$"
-decrease_indent_pattern = "(^|\\s+|;)(fi|done|esac|else|elif)\\b.*$"
-# make sure to test each line mode & block mode
+auto_indent_using_last_non_empty_line = false
+increase_indent_pattern = "^\\s*(\\b(else|elif)\\b|([^#]+\\b(do|then|in)\\b)|([\\w\\*]+\\)))\\s*$"
+decrease_indent_patterns = [
+ { pattern = "^\\s*elif\\b.*", valid_after = ["if", "elif"] },
+ { pattern = "^\\s*else\\b.*", valid_after = ["if", "elif", "for", "while"] },
+ { pattern = "^\\s*fi\\b.*", valid_after = ["if", "elif", "else"] },
+ { pattern = "^\\s*done\\b.*", valid_after = ["for", "while"] },
+ { pattern = "^\\s*esac\\b.*", valid_after = ["case"] },
+ { pattern = "^\\s*[\\w\\*]+\\)\\s*$", valid_after = ["case_item"] },
+]
+
+# We can't use decrease_indent_patterns simply for elif, because
+# there is bug in tree sitter which throws ERROR on if match.
+#
+# This is workaround. That means, elif will outdents with despite
+# of wrong context. Like using elif after else.
+decrease_indent_pattern = "(^|\\s+|;)(elif)\\b.*$"
@@ -1,12 +1,12 @@
-(function_definition
- "function"?
- body: (
- _
- "{" @start
- "}" @end
- )) @indent
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent
-(array
- "(" @start
- ")" @end
- ) @indent
+(function_definition) @start.function
+(if_statement) @start.if
+(elif_clause) @start.elif
+(else_clause) @start.else
+(for_statement) @start.for
+(while_statement) @start.while
+(case_statement) @start.case
+(case_item) @start.case_item