From 9938ebefbc58638b88f2fb5edb0cfb46cf7ee8b4 Mon Sep 17 00:00:00 2001 From: Oliver Azevedo Barnes Date: Wed, 4 Mar 2026 20:29:20 +0000 Subject: [PATCH] editor: Add support for no auto-indent on enter (#47751) Closes #47550 Changes the `auto_indent` setting from a boolean to an enum with three modes: - **`full`** (default): Adjusts indentation based on syntax context when typing (previous `true` behavior) - **`preserve_indent`**: Preserves the current line's indentation on new lines, but doesn't adjust based on syntax - **`none`**: No automatic indentation - new lines start at column 0 (previous `false` behavior) This gives users more control over indentation behavior. Previously, setting `auto_indent: false` would still preserve indentation on new lines, which was unexpected. Includes: - Settings migration from boolean to enum values - Settings UI dropdown renderer Release Notes: - Changed `auto_indent` setting from boolean to enum with `full`, `preserve_indent`, and `none` options Screenshot 2026-01-27 at 16 32 10 --------- Co-authored-by: MrSubidubi --- assets/settings/default.json | 7 +- crates/editor/src/editor.rs | 29 +++- crates/editor/src/editor_tests.rs | 159 +++++++++++++++++- crates/language/src/buffer.rs | 13 +- crates/language/src/language.rs | 2 +- crates/language/src/language_settings.rs | 6 +- crates/migrator/src/migrations.rs | 6 + .../src/migrations/m_2025_01_27/settings.rs | 27 +++ crates/migrator/src/migrator.rs | 86 ++++++++++ crates/settings/src/settings_store.rs | 10 +- crates/settings_content/src/language.rs | 36 +++- crates/settings_ui/src/page_data.rs | 2 +- crates/settings_ui/src/settings_ui.rs | 1 + 13 files changed, 355 insertions(+), 29 deletions(-) create mode 100644 crates/migrator/src/migrations/m_2025_01_27/settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 6593c3b192cb9ac388c67170fe20787bdbcf1bbc..0a824bbe93a0d68a23d934a63eb1fdab1e2f1b02 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -361,8 +361,11 @@ // bracket, brace, single or double quote characters. // For example, when you select text and type '(', Zed will surround the text with (). "use_auto_surround": true, - // Whether indentation should be adjusted based on the context whilst typing. - "auto_indent": true, + // Controls automatic indentation behavior when typing. + // - "syntax_aware": Adjusts indentation based on syntax context (default) + // - "preserve_indent": Preserves current line's indentation on new lines + // - "none": No automatic indentation + "auto_indent": "syntax_aware", // Whether indentation of pasted content should be adjusted based on the context. "auto_indent_on_paste": true, // Controls how the editor handles the autoclosed characters. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0d1238da21695738e4f6cedc54e172ad456c9bd6..3b18c9a447d8fb4569bbf331f1ba8e4602a555b9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5216,29 +5216,48 @@ impl Editor { extra_line_additional_indent, prevent_auto_indent, } => { + let auto_indent_mode = + buffer.language_settings_at(start, cx).auto_indent; + let preserve_indent = + auto_indent_mode != language::AutoIndentMode::None; + let apply_syntax_indent = + auto_indent_mode == language::AutoIndentMode::SyntaxAware; let capacity_for_delimiter = delimiter.as_deref().map(str::len).unwrap_or_default(); + let existing_indent_len = if preserve_indent { + existing_indent.len as usize + } else { + 0 + }; let extra_line_len = extra_line_additional_indent - .map(|i| 1 + existing_indent.len as usize + i.len as usize) + .map(|i| 1 + existing_indent_len + i.len as usize) .unwrap_or(0); let mut new_text = String::with_capacity( 1 + capacity_for_delimiter - + existing_indent.len as usize + + existing_indent_len + additional_indent.len as usize + extra_line_len, ); new_text.push('\n'); - new_text.extend(existing_indent.chars()); + if preserve_indent { + new_text.extend(existing_indent.chars()); + } new_text.extend(additional_indent.chars()); if let Some(delimiter) = &delimiter { new_text.push_str(delimiter); } if let Some(extra_indent) = extra_line_additional_indent { new_text.push('\n'); - new_text.extend(existing_indent.chars()); + if preserve_indent { + new_text.extend(existing_indent.chars()); + } new_text.extend(extra_indent.chars()); } - (start, new_text, *prevent_auto_indent) + ( + start, + new_text, + *prevent_auto_indent || !apply_syntax_indent, + ) } }; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 38abff942acf8717000090a90654f1117ba5005d..199cb0d3785a048f6390070d67546394bd89ff68 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10716,7 +10716,9 @@ async fn test_autoindent(cx: &mut TestAppContext) { #[gpui::test] async fn test_autoindent_disabled(cx: &mut TestAppContext) { - init_test(cx, |settings| settings.defaults.auto_indent = Some(false)); + init_test(cx, |settings| { + settings.defaults.auto_indent = Some(settings::AutoIndentMode::None) + }); let language = Arc::new( Language::new( @@ -10794,14 +10796,165 @@ async fn test_autoindent_disabled(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_autoindent_none_does_not_preserve_indentation_on_newline(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.auto_indent = Some(settings::AutoIndentMode::None) + }); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + hello + indented lineˇ + world + "}); + + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + + cx.assert_editor_state(indoc! {" + hello + indented line + ˇ + world + "}); +} + +#[gpui::test] +async fn test_autoindent_preserve_indent_maintains_indentation_on_newline(cx: &mut TestAppContext) { + // When auto_indent is "preserve_indent", pressing Enter on an indented line + // should preserve the indentation but not adjust based on syntax. + init_test(cx, |settings| { + settings.defaults.auto_indent = Some(settings::AutoIndentMode::PreserveIndent) + }); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + hello + indented lineˇ + world + "}); + + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + + // The new line SHOULD have the same indentation as the previous line + cx.assert_editor_state(indoc! {" + hello + indented line + ˇ + world + "}); +} + +#[gpui::test] +async fn test_autoindent_preserve_indent_does_not_apply_syntax_indent(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.auto_indent = Some(settings::AutoIndentMode::PreserveIndent) + }); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: false, // Disable extra newline behavior to isolate syntax indent test + }], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_indents_query(r#"(_ "{" "}" @end) @indent"#) + .unwrap(), + ); + + let buffer = + cx.new(|cx| Buffer::local("fn foo() {\n}", cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + // Position cursor at end of line containing `{` + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([MultiBufferOffset(10)..MultiBufferOffset(10)]) // After "fn foo() {" + }); + editor.newline(&Newline, window, cx); + + // With PreserveIndent, the new line should have 0 indentation (same as the fn line) + // NOT 4 spaces (which tree-sitter would add for being inside `{}`) + assert_eq!(editor.text(cx), "fn foo() {\n\n}"); + }); +} + +#[gpui::test] +async fn test_autoindent_syntax_aware_applies_syntax_indent(cx: &mut TestAppContext) { + // Companion test to show that SyntaxAware DOES apply tree-sitter indentation + init_test(cx, |settings| { + settings.defaults.auto_indent = Some(settings::AutoIndentMode::SyntaxAware) + }); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: false, // Disable extra newline behavior to isolate syntax indent test + }], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_indents_query(r#"(_ "{" "}" @end) @indent"#) + .unwrap(), + ); + + let buffer = + cx.new(|cx| Buffer::local("fn foo() {\n}", cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + // Position cursor at end of line containing `{` + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([MultiBufferOffset(10)..MultiBufferOffset(10)]) // After "fn foo() {" + }); + editor.newline(&Newline, window, cx); + + // With SyntaxAware, tree-sitter adds indentation for being inside `{}` + assert_eq!(editor.text(cx), "fn foo() {\n \n}"); + }); +} + #[gpui::test] async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.auto_indent = Some(true); + settings.defaults.auto_indent = Some(settings::AutoIndentMode::SyntaxAware); settings.languages.0.insert( "python".into(), LanguageSettingsContent { - auto_indent: Some(false), + auto_indent: Some(settings::AutoIndentMode::None), ..Default::default() }, ); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index eb9bb0827a7be9f4a725246c6d38777e340eee2c..d183615317ecaa481cda45d780c64b2ddf7ec833 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -4,7 +4,7 @@ use crate::{ DebuggerTextObject, LanguageScope, Outline, OutlineConfig, PLAIN_TEXT, RunnableCapture, RunnableTag, TextObject, TreeSitterOptions, diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup}, - language_settings::{LanguageSettings, language_settings}, + language_settings::{AutoIndentMode, LanguageSettings, language_settings}, outline::OutlineItem, row_chunk::RowChunks, syntax_map::{ @@ -2738,17 +2738,18 @@ impl Buffer { .filter(|((_, (range, _)), _)| { let language = before_edit.language_at(range.start); let language_id = language.map(|l| l.id()); - if let Some((cached_language_id, auto_indent)) = previous_setting + if let Some((cached_language_id, apply_syntax_indent)) = previous_setting && cached_language_id == language_id { - auto_indent + apply_syntax_indent } else { // The auto-indent setting is not present in editorconfigs, hence // we can avoid passing the file here. - let auto_indent = + let auto_indent_mode = language_settings(language.map(|l| l.name()), None, cx).auto_indent; - previous_setting = Some((language_id, auto_indent)); - auto_indent + let apply_syntax_indent = auto_indent_mode == AutoIndentMode::SyntaxAware; + previous_setting = Some((language_id, apply_syntax_indent)); + apply_syntax_indent } }) .map(|((ix, (range, _)), new_text)| { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 435d3d4e27998cb135dc3145ad7800ed8da97c9e..29b569ba1aa68fe83f3456a2eaf9911b4c83677d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -23,7 +23,7 @@ mod toolchain; pub mod buffer_tests; use crate::language_settings::SoftWrap; -pub use crate::language_settings::{EditPredictionsMode, IndentGuideSettings}; +pub use crate::language_settings::{AutoIndentMode, EditPredictionsMode, IndentGuideSettings}; use anyhow::{Context as _, Result}; use async_trait::async_trait; use collections::{HashMap, HashSet, IndexSet}; diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 9a379697e8bddf9dc71d3d340d5e2a92d8b4405e..f2c55fd1e8a3b8bf5b6c2dd8ea24d1343385fa78 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -12,7 +12,7 @@ use itertools::{Either, Itertools}; use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens}; pub use settings::{ - CompletionSettingsContent, EditPredictionPromptFormat, EditPredictionProvider, + AutoIndentMode, CompletionSettingsContent, EditPredictionPromptFormat, EditPredictionProvider, EditPredictionsMode, FormatOnSave, Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LspInsertMode, RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, @@ -144,8 +144,8 @@ pub struct LanguageSettings { /// Whether to use additional LSP queries to format (and amend) the code after /// every "trigger" symbol input, defined by LSP server capabilities. pub use_on_type_format: bool, - /// Whether indentation should be adjusted based on the context whilst typing. - pub auto_indent: bool, + /// Controls automatic indentation behavior when typing. + pub auto_indent: AutoIndentMode, /// Whether indentation of pasted content should be adjusted based on the context. pub auto_indent_on_paste: bool, /// Controls how the editor handles the autoclosed characters. diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index d10116be6032486c92d9f27afcf922178463e151..ec33b6a53b3c598842aa29b6e2c31c08c7b11558 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -275,6 +275,12 @@ pub(crate) mod m_2025_12_15 { pub(crate) use settings::SETTINGS_PATTERNS; } +pub(crate) mod m_2025_01_27 { + mod settings; + + pub(crate) use settings::make_auto_indent_an_enum; +} + pub(crate) mod m_2026_02_02 { mod settings; diff --git a/crates/migrator/src/migrations/m_2025_01_27/settings.rs b/crates/migrator/src/migrations/m_2025_01_27/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..e8df2aa8aabed4daaae3e45e97532c1ce3557dfe --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_01_27/settings.rs @@ -0,0 +1,27 @@ +use anyhow::Result; +use serde_json::Value; + +use crate::migrations::migrate_language_setting; + +pub fn make_auto_indent_an_enum(value: &mut Value) -> Result<()> { + migrate_language_setting(value, migrate_auto_indent) +} + +fn migrate_auto_indent(value: &mut Value, _path: &[&str]) -> Result<()> { + let Some(auto_indent) = value + .as_object_mut() + .and_then(|obj| obj.get_mut("auto_indent")) + else { + return Ok(()); + }; + + *auto_indent = match auto_indent { + Value::Bool(true) => Value::String("syntax_aware".to_string()), + Value::Bool(false) => Value::String("none".to_string()), + Value::String(s) if s == "syntax_aware" || s == "preserve_indent" || s == "none" => { + return Ok(()); + } + _ => anyhow::bail!("Expected auto_indent to be a boolean or valid enum value"), + }; + Ok(()) +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 8b501020a559c74d81c5ad5b37e1adf60a964927..f208faf163aaf425127791f781d4569a737870ff 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -232,6 +232,7 @@ pub fn migrate_settings(text: &str) -> Result> { migrations::m_2025_12_15::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_12_15, ), + MigrationType::Json(migrations::m_2025_01_27::make_auto_indent_an_enum), MigrationType::Json( migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions, ), @@ -2606,6 +2607,91 @@ mod tests { ); } + #[test] + fn test_make_auto_indent_an_enum() { + // Empty settings should not change + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_01_27::make_auto_indent_an_enum, + )], + &r#"{ }"#.unindent(), + None, + ); + + // true should become "syntax_aware" + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_01_27::make_auto_indent_an_enum, + )], + &r#"{ + "auto_indent": true + }"# + .unindent(), + Some( + &r#"{ + "auto_indent": "syntax_aware" + }"# + .unindent(), + ), + ); + + // false should become "none" + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_01_27::make_auto_indent_an_enum, + )], + &r#"{ + "auto_indent": false + }"# + .unindent(), + Some( + &r#"{ + "auto_indent": "none" + }"# + .unindent(), + ), + ); + + // Already valid enum values should not change + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_01_27::make_auto_indent_an_enum, + )], + &r#"{ + "auto_indent": "preserve_indent" + }"# + .unindent(), + None, + ); + + // Should also work inside languages + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_01_27::make_auto_indent_an_enum, + )], + &r#"{ + "auto_indent": true, + "languages": { + "Python": { + "auto_indent": false + } + } + }"# + .unindent(), + Some( + &r#"{ + "auto_indent": "syntax_aware", + "languages": { + "Python": { + "auto_indent": "none" + } + } + }"# + .unindent(), + ), + ); + } + #[test] fn test_move_edit_prediction_provider_to_edit_predictions() { assert_migrate_settings_with_migrations( diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 8551fc2edd53df66965b18abbe91f7083dd08461..26425faf113a9dc0f52ad04809dc71c2f89eeb69 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1706,7 +1706,7 @@ mod tests { r#"{ "languages": { "JSON": { - "auto_indent": true + "auto_indent": "syntax_aware" } } }"# @@ -1716,12 +1716,12 @@ mod tests { .languages_mut() .get_mut("JSON") .unwrap() - .auto_indent = Some(false); + .auto_indent = Some(crate::AutoIndentMode::None); settings.languages_mut().insert( "Rust".into(), LanguageSettingsContent { - auto_indent: Some(true), + auto_indent: Some(crate::AutoIndentMode::SyntaxAware), ..Default::default() }, ); @@ -1729,10 +1729,10 @@ mod tests { r#"{ "languages": { "Rust": { - "auto_indent": true + "auto_indent": "syntax_aware" }, "JSON": { - "auto_indent": false + "auto_indent": "none" } } }"# diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index a8d68fea99c024830ee45c66ec5d7d641aa4c250..fba636ee28be121a15da4b3d50046c53c0bdd5b3 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -369,6 +369,32 @@ pub enum EditPredictionsMode { Eager, } +/// Controls the soft-wrapping behavior in the editor. +#[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum AutoIndentMode { + /// Adjusts indentation based on syntax context when typing. + /// Uses tree-sitter to analyze code structure and indent accordingly. + SyntaxAware, + /// Preserve the indentation of the current line when creating new lines, + /// but don't adjust based on syntax context. + PreserveIndent, + /// No automatic indentation. New lines start at column 0. + None, +} + /// Controls the soft-wrapping behavior in the editor. #[derive( Copy, @@ -571,10 +597,14 @@ pub struct LanguageSettingsContent { /// /// Default: true pub linked_edits: Option, - /// Whether indentation should be adjusted based on the context whilst typing. + /// Controls automatic indentation behavior when typing. /// - /// Default: true - pub auto_indent: Option, + /// - "syntax_aware": Adjusts indentation based on syntax context (default) + /// - "preserve_indent": Preserves current line's indentation on new lines + /// - "none": No automatic indentation + /// + /// Default: syntax_aware + pub auto_indent: Option, /// Whether indentation of pasted content should be adjusted based on the context. /// /// Default: true diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index afc84a9f9b91e32f3a110e19dc78db5634369458..dbac4d7ba350fcff07016a2ccfa483f3d84472c7 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -7405,7 +7405,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { }), SettingsPageItem::SettingItem(SettingItem { title: "Auto Indent", - description: "Whether indentation should be adjusted based on the context whilst typing.", + description: "Controls automatic indentation behavior when typing.", field: Box::new(SettingField { json_path: Some("languages.$(language).auto_indent"), pick: |settings_content| { diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index def4c7630869cae69c539e1d83660e8df9a18318..9d7fe83736be8d1d9ed79d85708c5ed0574b7e3a 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -474,6 +474,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown)