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

<img width="1373" height="802" alt="Screenshot 2026-01-27 at 16 32 10"
src="https://github.com/user-attachments/assets/b629e1d8-7359-4853-8222-abfa71d6ebe2"
/>

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
This commit is contained in:
Oliver Azevedo BarnesandMrSubidubi authored and GitHub committed 2026-03-04 20:29:20 +00:00
1 parent 4d42d3a6b0
commit 9938ebefbc
13 files changed
+355 -29

No files matched your search

+5 -2
View File
@@ -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.
+24 -5
View File
@@ -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,
)
}
};
+156 -3
View File
@@ -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()
},
);
+7 -6
View File
@@ -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)| {
+1 -1
View File
@@ -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};
+3 -3
View File
@@ -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.
+6
View File
@@ -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(())
}
+86
View File
@@ -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(
+5 -5
View File
@@ -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"
}
}
}"#
+33 -3
View File
@@ -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
+1 -1
View File
@@ -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| {
+1
View File
@@ -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)