Detailed changes
@@ -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.
@@ -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,
+ )
}
};
@@ -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::<crate::EditorEvent>(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::<crate::EditorEvent>(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()
},
);
@@ -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)| {
@@ -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};
@@ -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.
@@ -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;
@@ -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(())
+}
@@ -232,6 +232,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
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(
@@ -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"
}
}
}"#
@@ -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<bool>,
- /// Whether indentation should be adjusted based on the context whilst typing.
+ /// Controls automatic indentation behavior when typing.
///
- /// Default: true
- pub auto_indent: Option<bool>,
+ /// - "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<AutoIndentMode>,
/// Whether indentation of pasted content should be adjusted based on the context.
///
/// Default: true
@@ -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| {
@@ -474,6 +474,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::CurrentLineHighlight>(render_dropdown)
.add_basic_renderer::<settings::ShowWhitespaceSetting>(render_dropdown)
.add_basic_renderer::<settings::SoftWrap>(render_dropdown)
+ .add_basic_renderer::<settings::AutoIndentMode>(render_dropdown)
.add_basic_renderer::<settings::ScrollBeyondLastLine>(render_dropdown)
.add_basic_renderer::<settings::SnippetSortOrder>(render_dropdown)
.add_basic_renderer::<settings::ClosePosition>(render_dropdown)