Coalesce consecutive spaces in new buffer tab titles (#32363)

Joseph T. Lyons created

VS Code has a behavior where it coalesces consecutive spaces in new
buffer tab titles, which I quite like. This presents the content better
and allows more meaningful content to be displayed, as consecutive
spaces don't count towards the 40 character limit.

VS Code

<img width="1013" alt="SCR-20250608-uelt"
src="https://github.com/user-attachments/assets/71a1fd4b-a506-4eab-b6a4-66096a12f1ad"
/>

Zed

<img width="1136" alt="SCR-20250608-ueif"
src="https://github.com/user-attachments/assets/f40fc3c9-0f0f-471d-93ed-be9568fbe778"
/>


Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs                   |  2 
crates/multi_buffer/src/multi_buffer.rs       | 67 +++++++++++++++-----
crates/multi_buffer/src/multi_buffer_tests.rs | 18 ++++-
3 files changed, 64 insertions(+), 23 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -18858,7 +18858,7 @@ impl Editor {
                 cx.emit(EditorEvent::BufferEdited);
                 cx.emit(SearchEvent::MatchesInvalidated);
                 if *singleton_buffer_edited {
-                    if let Some(buffer) = multibuffer.read(cx).as_singleton() {
+                    if let Some(buffer) = edited_buffer {
                         if buffer.read(cx).file().is_none() {
                             cx.emit(EditorEvent::TitleChanged);
                         }

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -2600,27 +2600,58 @@ impl MultiBuffer {
             return title.into();
         }
 
-        self.as_singleton()
-            .and_then(|buffer| {
-                let buffer = buffer.read(cx);
+        if let Some(buffer) = self.as_singleton() {
+            let buffer = buffer.read(cx);
+
+            if let Some(file) = buffer.file() {
+                return file.file_name(cx).to_string_lossy();
+            }
+
+            if let Some(title) = self.buffer_based_title(buffer) {
+                return title;
+            }
+        };
+
+        "untitled".into()
+    }
+
+    fn buffer_based_title(&self, buffer: &Buffer) -> Option<Cow<str>> {
+        let mut is_leading_whitespace = true;
+        let mut count = 0;
+        let mut prev_was_space = false;
+        let mut title = String::new();
+
+        for ch in buffer.snapshot().chars() {
+            if is_leading_whitespace && ch.is_whitespace() {
+                continue;
+            }
 
-                if let Some(file) = buffer.file() {
-                    return Some(file.file_name(cx).to_string_lossy());
+            is_leading_whitespace = false;
+
+            if ch == '\n' || count >= 40 {
+                break;
+            }
+
+            if ch.is_whitespace() {
+                if !prev_was_space {
+                    title.push(' ');
+                    count += 1;
+                    prev_was_space = true;
                 }
+            } else {
+                title.push(ch);
+                count += 1;
+                prev_was_space = false;
+            }
+        }
 
-                let title = buffer
-                    .snapshot()
-                    .chars()
-                    .skip_while(|ch| ch.is_whitespace())
-                    .take_while(|&ch| ch != '\n')
-                    .take(40)
-                    .collect::<String>()
-                    .trim_end()
-                    .to_string();
-
-                (!title.is_empty()).then(|| title.into())
-            })
-            .unwrap_or("untitled".into())
+        let title = title.trim_end().to_string();
+
+        if !title.is_empty() {
+            return Some(title.into());
+        }
+
+        None
     }
 
     pub fn set_title(&mut self, title: String, cx: &mut Context<Self>) {

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -3686,10 +3686,20 @@ fn test_new_empty_buffer_takes_trimmed_first_line_for_title(cx: &mut App) {
 
 #[gpui::test]
 fn test_new_empty_buffer_uses_truncated_first_line_for_title(cx: &mut App) {
-    let title_after = ["a", "b", "c", "d"]
-        .map(|letter| letter.repeat(10))
-        .join("");
-    let title = format!("{}{}", title_after, "e".repeat(10));
+    let title = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
+    let title_after = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd";
+    let buffer = cx.new(|cx| Buffer::local(title, cx));
+    let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
+
+    assert_eq!(multibuffer.read(cx).title(cx), title_after);
+}
+
+#[gpui::test]
+fn test_new_empty_buffer_uses_truncated_first_line_for_title_after_merging_adjacent_spaces(
+    cx: &mut App,
+) {
+    let title = "aaaaaaaaaabbbbbbbbbb    ccccccccccddddddddddeeeeeeeeee";
+    let title_after = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddd";
     let buffer = cx.new(|cx| Buffer::local(title, cx));
     let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));