From 0079c99c2ccfdac54b879e5efeec10344f3bee03 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sun, 18 May 2025 07:29:25 +0530 Subject: [PATCH] editor: Add python indentation tests (#30902) This PR add tests for a recent PR: [language: Fix indent suggestions for significant indented languages like Python](https://github.com/zed-industries/zed/pull/29625) It also covers cases from past related issues so that we don't end up circling back to them on future fixes. - [Python incorrect auto-indentation for except:](https://github.com/zed-industries/zed/issues/10832) - [Python for/while...else indention overridden by if statement ](https://github.com/zed-industries/zed/issues/30795) - [Python: erroneous indent on newline when comment ends in :](https://github.com/zed-industries/zed/issues/25416) - [Newline in Python file does not indent ](https://github.com/zed-industries/zed/issues/16288) - [Tab Indentation works incorrectly when there are multiple cursors](https://github.com/zed-industries/zed/issues/26157) Release Notes: - N/A --- Cargo.lock | 1 + crates/editor/Cargo.toml | 1 + crates/editor/src/editor_tests.rs | 325 ++++++++++++++++++++++++ crates/languages/src/python/indents.scm | 3 + 4 files changed, 330 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3443c4e40b..6d9ad27f38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4702,6 +4702,7 @@ dependencies = [ "theme", "time", "tree-sitter-html", + "tree-sitter-python", "tree-sitter-rust", "tree-sitter-typescript", "ui", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 716fc602bd..6edc7a5f6a 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -79,6 +79,7 @@ theme.workspace = true tree-sitter-html = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } +tree-sitter-python = { workspace = true, optional = true } unicode-segmentation.workspace = true unicode-script.workspace = true unindent = { workspace = true, optional = true } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 8ba938c907..b00597cc8c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -26,6 +26,7 @@ use language::{ AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, LanguageSettingsContent, LspInsertMode, PrettierSettings, }, + tree_sitter_python, }; use language_settings::{Formatter, FormatterList, IndentGuideSettings}; use lsp::CompletionParams; @@ -20210,6 +20211,330 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = languages::language("python", tree_sitter_python::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`, `with` and `for` + cx.set_state(indoc! {" + def main(): + ˇ for item in items: + ˇ while item.active: + ˇ if item.value > 10: + ˇ continue + ˇ elif item.value < 0: + ˇ break + ˇ else: + ˇ with item.context() as ctx: + ˇ yield count + ˇ else: + ˇ log('while else') + ˇ else: + ˇ log('for else') + "}); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + cx.assert_editor_state(indoc! {" + def main(): + ˇfor item in items: + ˇwhile item.active: + ˇif item.value > 10: + ˇcontinue + ˇelif item.value < 0: + ˇbreak + ˇelse: + ˇwith item.context() as ctx: + ˇyield count + ˇelse: + ˇlog('while else') + ˇelse: + ˇlog('for else') + "}); + // test relative indent is preserved when tab + // for `if`, `elif`, `else`, `while`, `with` and `for` + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + cx.assert_editor_state(indoc! {" + def main(): + ˇfor item in items: + ˇwhile item.active: + ˇif item.value > 10: + ˇcontinue + ˇelif item.value < 0: + ˇbreak + ˇelse: + ˇwith item.context() as ctx: + ˇyield count + ˇelse: + ˇlog('while else') + ˇelse: + ˇlog('for else') + "}); + + // test cursor move to start of each line on tab + // for `try`, `except`, `else`, `finally`, `match` and `def` + cx.set_state(indoc! {" + def main(): + ˇ try: + ˇ fetch() + ˇ except ValueError: + ˇ handle_error() + ˇ else: + ˇ match value: + ˇ case _: + ˇ finally: + ˇ def status(): + ˇ return 0 + "}); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + cx.assert_editor_state(indoc! {" + def main(): + ˇtry: + ˇfetch() + ˇexcept ValueError: + ˇhandle_error() + ˇelse: + ˇmatch value: + ˇcase _: + ˇfinally: + ˇdef status(): + ˇreturn 0 + "}); + // test relative indent is preserved when tab + // for `try`, `except`, `else`, `finally`, `match` and `def` + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + cx.assert_editor_state(indoc! {" + def main(): + ˇtry: + ˇfetch() + ˇexcept ValueError: + ˇhandle_error() + ˇelse: + ˇmatch value: + ˇcase _: + ˇfinally: + ˇdef status(): + ˇreturn 0 + "}); +} + +#[gpui::test] +async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = languages::language("python", tree_sitter_python::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! {" + def main(): + if i == 2: + return + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("else:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + if i == 2: + return + else:ˇ + "}); + + // test `except` auto outdents when typed inside `try` block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("except:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + except:ˇ + "}); + + // test `else` auto outdents when typed inside `except` block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + except: + j = 2 + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("else:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + except: + j = 2 + else:ˇ + "}); + + // test `finally` auto outdents when typed inside `else` block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + except: + j = 2 + else: + k = 2 + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("finally:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + except: + j = 2 + else: + k = 2 + finally:ˇ + "}); + + // TODO: test `except` auto outdents when typed inside `try` block right after for block + // cx.set_state(indoc! {" + // def main(): + // try: + // for i in range(n): + // pass + // ˇ + // "}); + // cx.update_editor(|editor, window, cx| { + // editor.handle_input("except:", window, cx); + // }); + // cx.assert_editor_state(indoc! {" + // def main(): + // try: + // for i in range(n): + // pass + // except:ˇ + // "}); + + // TODO: test `else` auto outdents when typed inside `except` block right after for block + // cx.set_state(indoc! {" + // def main(): + // try: + // i = 2 + // except: + // for i in range(n): + // pass + // ˇ + // "}); + // cx.update_editor(|editor, window, cx| { + // editor.handle_input("else:", window, cx); + // }); + // cx.assert_editor_state(indoc! {" + // def main(): + // try: + // i = 2 + // except: + // for i in range(n): + // pass + // else:ˇ + // "}); + + // TODO: test `finally` auto outdents when typed inside `else` block right after for block + // cx.set_state(indoc! {" + // def main(): + // try: + // i = 2 + // except: + // j = 2 + // else: + // for i in range(n): + // pass + // ˇ + // "}); + // cx.update_editor(|editor, window, cx| { + // editor.handle_input("finally:", window, cx); + // }); + // cx.assert_editor_state(indoc! {" + // def main(): + // try: + // i = 2 + // except: + // j = 2 + // else: + // for i in range(n): + // pass + // finally:ˇ + // "}); + + // test `else` stays at correct indent when typed after `for` block + cx.set_state(indoc! {" + def main(): + for i in range(10): + if i == 3: + break + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("else:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + for i in range(10): + if i == 3: + break + else:ˇ + "}); +} + +#[gpui::test] +async fn test_indent_on_newline_for_python(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("python", tree_sitter_python::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 in curly brackets + cx.set_state(indoc! {" + {ˇ} + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + { + ˇ + } + "}); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/languages/src/python/indents.scm b/crates/languages/src/python/indents.scm index f5fe106c53..34557f3b2a 100644 --- a/crates/languages/src/python/indents.scm +++ b/crates/languages/src/python/indents.scm @@ -1,3 +1,6 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent + (function_definition ":" @start body: (block) @indent