From f7a0971d2b9c366338dabad13fb494fe0a2c83b1 Mon Sep 17 00:00:00 2001
From: kitt <11167504+kitt-cat@users.noreply.github.com>
Date: Mon, 20 Oct 2025 14:24:41 -0700
Subject: [PATCH] Add line endings indicator in status bar (#39609)
Closes #5294
This PR adds a line ending indicator to the status bar, hidden by
default as discussed in
https://github.com/zed-industries/zed/issues/5294.
### Changes
- 8b063a22d8700bed9c93989b9e0f6a064b2e86cf add the indicator and
`status_bar.line_endings_button` setting.
- ~~9926237b709dd4e25ce58d558fd385d63b405f3b changes
`status_bar.line_endings_button` from a boolean to an enum:~~
show details
- `always` Always show line endings indicator.
- `non_native` Indicate when line endings do not match the current
platform.
- `lf_only` Indicate when using unix-style (LF) line endings only.
- `crlf_only` Indicate when using windows-style (CRLF) line endings
only.
- `never` Do not show line endings indicator.
I know this many options might be overdoing it, but I was torn between
the pleasant default of `non_native` and the simplicity of `lf_only` /
`crlf_only`.
My thinking was if one is developing on a project which exclusively uses
one line-ending style or the other, it would be nice to be able to
configure no-indicator-in-the-happy-case behavior regardless of the
platform zed is running on. But I'm not really familiar with any
projects that use exclusively CRLF line endings in practice. Is this a
scenario worth supporting or just something I dreamed up?
- 01174191e4cf337069e7a31b0f0432ae94c52515 rename the action context for
`line ending: Toggle` -> `line ending selector: Toggle`.
When running the action in the command palette with the old name I felt
surprised to be greeted with an additional menu, with the new name it
feels more predictable (plus now it matches
`language_selector::Toggle`!)
### Future work
Hidden status bar items still get padding, creating inconsistent spacing
(and it kind of stands out where I placed the line-endings button):
I started a new follow-up PR to address that:
https://github.com/zed-industries/zed/pull/39992
Release Notes:
- Added line ending indicator to the status bar (disabled by default;
enabled by setting `status_bar.line_endings_button` to `true`)
---
assets/settings/default.json | 4 +-
.../src/line_ending_indicator.rs | 70 +++++++++++++++++++
.../src/line_ending_selector.rs | 10 +--
.../src/settings_content/workspace.rs | 4 ++
crates/text/src/text.rs | 7 ++
crates/workspace/src/workspace_settings.rs | 2 +
crates/zed/src/zed.rs | 5 +-
docs/src/configuring-zed.md | 3 +-
docs/src/visual-customization.md | 4 ++
9 files changed, 101 insertions(+), 8 deletions(-)
create mode 100644 crates/line_ending_selector/src/line_ending_indicator.rs
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 327b35c4b197818c09a065e2b6203430a6150665..c110b169b7cd18098de9945b43f72ac1fa0cff19 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1350,7 +1350,9 @@
// Whether to show the active language button in the status bar.
"active_language_button": true,
// Whether to show the cursor position button in the status bar.
- "cursor_position_button": true
+ "cursor_position_button": true,
+ // Whether to show active line endings button in the status bar.
+ "line_endings_button": false
},
// Settings specific to the terminal
"terminal": {
diff --git a/crates/line_ending_selector/src/line_ending_indicator.rs b/crates/line_ending_selector/src/line_ending_indicator.rs
new file mode 100644
index 0000000000000000000000000000000000000000..042630056a4cad93497e7b35cab7c82c1ea643e3
--- /dev/null
+++ b/crates/line_ending_selector/src/line_ending_indicator.rs
@@ -0,0 +1,70 @@
+use editor::Editor;
+use gpui::{Entity, Subscription, WeakEntity};
+use language::LineEnding;
+use ui::{Tooltip, prelude::*};
+use workspace::{StatusBarSettings, StatusItemView, item::ItemHandle, item::Settings};
+
+use crate::{LineEndingSelector, Toggle};
+
+#[derive(Default)]
+pub struct LineEndingIndicator {
+ line_ending: Option,
+ active_editor: Option>,
+ _observe_active_editor: Option,
+}
+
+impl LineEndingIndicator {
+ fn update(&mut self, editor: Entity, _: &mut Window, cx: &mut Context) {
+ self.line_ending = None;
+ self.active_editor = None;
+
+ if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) {
+ let line_ending = buffer.read(cx).line_ending();
+ self.line_ending = Some(line_ending);
+ self.active_editor = Some(editor.downgrade());
+ }
+
+ cx.notify();
+ }
+}
+
+impl Render for LineEndingIndicator {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ if !StatusBarSettings::get_global(cx).line_endings_button {
+ return div();
+ }
+
+ div().when_some(self.line_ending.as_ref(), |el, line_ending| {
+ el.child(
+ Button::new("change-line-ending", line_ending.label())
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener(|this, _, window, cx| {
+ if let Some(editor) = this.active_editor.as_ref() {
+ LineEndingSelector::toggle(editor, window, cx);
+ }
+ }))
+ .tooltip(|window, cx| {
+ Tooltip::for_action("Select Line Ending", &Toggle, window, cx)
+ }),
+ )
+ })
+ }
+}
+
+impl StatusItemView for LineEndingIndicator {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) {
+ self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
+ self.update(editor, window, cx);
+ } else {
+ self.line_ending = None;
+ self._observe_active_editor = None;
+ }
+ cx.notify();
+ }
+}
diff --git a/crates/line_ending_selector/src/line_ending_selector.rs b/crates/line_ending_selector/src/line_ending_selector.rs
index 7f75a1ebe3550595c8fa78643ef5446ab2fa3a44..504c327a349c97214e801f6bd375d61c7847f2be 100644
--- a/crates/line_ending_selector/src/line_ending_selector.rs
+++ b/crates/line_ending_selector/src/line_ending_selector.rs
@@ -1,6 +1,9 @@
+mod line_ending_indicator;
+
use editor::Editor;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, actions};
use language::{Buffer, LineEnding};
+pub use line_ending_indicator::LineEndingIndicator;
use picker::{Picker, PickerDelegate};
use project::Project;
use std::sync::Arc;
@@ -9,7 +12,7 @@ use util::ResultExt;
use workspace::ModalView;
actions!(
- line_ending,
+ line_ending_selector,
[
/// Toggles the line ending selector modal.
Toggle
@@ -172,10 +175,7 @@ impl PickerDelegate for LineEndingSelectorDelegate {
_: &mut Context>,
) -> Option {
let line_ending = self.matches.get(ix)?;
- let label = match line_ending {
- LineEnding::Unix => "LF",
- LineEnding::Windows => "CRLF",
- };
+ let label = line_ending.label();
let mut list_item = ListItem::new(ix)
.inset(true)
diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs
index 577f8fa4f996b2a808bdc785c56210e766dab2fb..a4e36ef8358487dbd4f3e5696eb52c5da9d28eb9 100644
--- a/crates/settings/src/settings_content/workspace.rs
+++ b/crates/settings/src/settings_content/workspace.rs
@@ -380,6 +380,10 @@ pub struct StatusBarSettingsContent {
///
/// Default: true
pub cursor_position_button: Option,
+ /// Whether to show active line endings button in the status bar.
+ ///
+ /// Default: false
+ pub line_endings_button: Option,
}
#[derive(
diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs
index 42d9a48430590302f96a1476bdbc3b876ed8f2f6..d78dfbea7dc0d3ac65005855eaffadee37fda584 100644
--- a/crates/text/src/text.rs
+++ b/crates/text/src/text.rs
@@ -3267,6 +3267,13 @@ impl LineEnding {
}
}
+ pub fn label(&self) -> &'static str {
+ match self {
+ LineEnding::Unix => "LF",
+ LineEnding::Windows => "CRLF",
+ }
+ }
+
pub fn detect(text: &str) -> Self {
let mut max_ix = cmp::min(text.len(), 1000);
while !text.is_char_boundary(max_ix) {
diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs
index 6ed2aeb5cb85ae5728f683b3c3d7dfbdf86f0c6a..f061227f2cb264b1be1234364ca1e8de7a462e86 100644
--- a/crates/workspace/src/workspace_settings.rs
+++ b/crates/workspace/src/workspace_settings.rs
@@ -126,6 +126,7 @@ pub struct StatusBarSettings {
pub show: bool,
pub active_language_button: bool,
pub cursor_position_button: bool,
+ pub line_endings_button: bool,
}
impl Settings for StatusBarSettings {
@@ -135,6 +136,7 @@ impl Settings for StatusBarSettings {
show: status_bar.show.unwrap(),
active_language_button: status_bar.active_language_button.unwrap(),
cursor_position_button: status_bar.cursor_position_button.unwrap(),
+ line_endings_button: status_bar.line_endings_button.unwrap(),
}
}
}
diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs
index a9b28229de20b8d24e481482e1441482e5d36212..1416a74e1697324213c11e1bbc51fd2d8a6bf91b 100644
--- a/crates/zed/src/zed.rs
+++ b/crates/zed/src/zed.rs
@@ -417,6 +417,8 @@ pub fn initialize_workspace(
let cursor_position =
cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
+ let line_ending_indicator =
+ cx.new(|_| line_ending_selector::LineEndingIndicator::default());
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(search_button, window, cx);
status_bar.add_left_item(lsp_button, window, cx);
@@ -425,6 +427,7 @@ pub fn initialize_workspace(
status_bar.add_right_item(edit_prediction_button, window, cx);
status_bar.add_right_item(active_buffer_language, window, cx);
status_bar.add_right_item(active_toolchain_language, window, cx);
+ status_bar.add_right_item(line_ending_indicator, window, cx);
status_bar.add_right_item(vim_mode_indicator, window, cx);
status_bar.add_right_item(cursor_position, window, cx);
status_bar.add_right_item(image_info, window, cx);
@@ -4669,7 +4672,7 @@ mod tests {
"keymap_editor",
"keystroke_input",
"language_selector",
- "line_ending",
+ "line_ending_selector",
"lsp_tool",
"markdown",
"menu",
diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md
index 4fb65d14118d637d7123998e53da9aa40dc7a84c..efc4538c0e5286a053a89916c90548796ba619d0 100644
--- a/docs/src/configuring-zed.md
+++ b/docs/src/configuring-zed.md
@@ -1498,7 +1498,8 @@ Positive `integer` value between 1 and 32. Values outside of this range will be
```json [settings]
"status_bar": {
"active_language_button": true,
- "cursor_position_button": true
+ "cursor_position_button": true,
+ "line_endings_button": false
},
```
diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md
index 89cb3ec1929e6c71f8f832818ef6c54c0219bff2..a31f4428cd9d554ce366e182da605a71eefe6eec 100644
--- a/docs/src/visual-customization.md
+++ b/docs/src/visual-customization.md
@@ -319,6 +319,10 @@ TBD: Centered layout related settings
// Clicking the button brings up an input for jumping to a line and column.
// Defaults to true.
"cursor_position_button": true,
+ // Show/hide a button that displays the buffer's line-ending mode.
+ // Clicking the button brings up the line-ending selector.
+ // Defaults to false.
+ "line_endings_button": false
},
"global_lsp_settings": {
// Show/hide the LSP button in the status bar.