Merge branch 'main' into copilot

Mikayla Maki created

Change summary

Cargo.lock                                      |   3 
crates/breadcrumbs/Cargo.toml                   |   1 
crates/breadcrumbs/src/breadcrumbs.rs           |  66 +++-
crates/editor/src/display_map.rs                |  28 -
crates/editor/src/display_map/suggestion_map.rs |  26 +
crates/editor/src/display_map/tab_map.rs        | 276 ++++++++++++++----
crates/editor/src/display_map/wrap_map.rs       |   3 
crates/editor/src/editor_tests.rs               |   2 
crates/editor/src/items.rs                      |  12 
crates/language/src/language.rs                 |   6 
crates/pando/Cargo.toml                         |  21 -
crates/pando/src/file_format.rs                 |   0 
crates/pando/src/pando.rs                       |  15 -
crates/terminal_view/src/terminal_view.rs       |   2 
crates/theme/src/theme.rs                       |   3 
crates/workspace/src/pane.rs                    |  10 
crates/workspace/src/toolbar.rs                 |  13 
crates/zed/Cargo.toml                           |   2 
styles/src/styleTree/app.ts                     |   6 
styles/src/styleTree/workspace.ts               |  17 +
20 files changed, 349 insertions(+), 163 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -785,6 +785,7 @@ dependencies = [
  "gpui",
  "itertools",
  "language",
+ "outline",
  "project",
  "search",
  "settings",
@@ -8484,7 +8485,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
 name = "zed"
-version = "0.80.0"
+version = "0.81.0"
 dependencies = [
  "activity_indicator",
  "anyhow",

crates/breadcrumbs/Cargo.toml πŸ”—

@@ -18,6 +18,7 @@ search = { path = "../search" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
+outline = { path = "../outline" }
 itertools = "0.10"
 
 [dev-dependencies]

crates/breadcrumbs/src/breadcrumbs.rs πŸ”—

@@ -1,5 +1,6 @@
 use gpui::{
-    elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
+    elements::*, AppContext, Entity, MouseButton, RenderContext, Subscription, View, ViewContext,
+    ViewHandle,
 };
 use itertools::Itertools;
 use search::ProjectSearchView;
@@ -14,6 +15,7 @@ pub enum Event {
 }
 
 pub struct Breadcrumbs {
+    pane_focused: bool,
     active_item: Option<Box<dyn ItemHandle>>,
     project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
@@ -22,6 +24,7 @@ pub struct Breadcrumbs {
 impl Breadcrumbs {
     pub fn new() -> Self {
         Self {
+            pane_focused: false,
             active_item: Default::default(),
             subscription: Default::default(),
             project_search: Default::default(),
@@ -39,24 +42,53 @@ impl View for Breadcrumbs {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let active_item = match &self.active_item {
+            Some(active_item) => active_item,
+            None => return Empty::new().boxed(),
+        };
+        let not_editor = active_item.downcast::<editor::Editor>().is_none();
+
         let theme = cx.global::<Settings>().theme.clone();
-        if let Some(breadcrumbs) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.breadcrumbs(&theme, cx))
-        {
-            Flex::row()
-                .with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
-                    Label::new(" βŒͺ ", theme.breadcrumbs.text.clone()).boxed()
-                }))
-                .contained()
-                .with_style(theme.breadcrumbs.container)
+        let style = &theme.workspace.breadcrumbs;
+
+        let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
+            Some(breadcrumbs) => breadcrumbs,
+            None => return Empty::new().boxed(),
+        };
+
+        let crumbs = Flex::row()
+            .with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
+                Label::new(" βŒͺ ", style.default.text.clone()).boxed()
+            }))
+            .constrained()
+            .with_height(theme.workspace.breadcrumb_height)
+            .contained();
+
+        if not_editor || !self.pane_focused {
+            return crumbs
+                .with_style(style.default.container)
                 .aligned()
                 .left()
-                .boxed()
-        } else {
-            Empty::new().boxed()
+                .boxed();
         }
+
+        MouseEventHandler::<Breadcrumbs>::new(0, cx, |state, _| {
+            let style = style.style_for(state, false);
+            crumbs.with_style(style.container).boxed()
+        })
+        .on_click(MouseButton::Left, |_, cx| {
+            cx.dispatch_action(outline::Toggle);
+        })
+        .with_tooltip::<Breadcrumbs, _>(
+            0,
+            "Show symbol outline".to_owned(),
+            Some(Box::new(outline::Toggle)),
+            theme.tooltip.clone(),
+            cx,
+        )
+        .aligned()
+        .left()
+        .boxed()
     }
 }
 
@@ -103,4 +135,8 @@ impl ToolbarItemView for Breadcrumbs {
             current_location
         }
     }
+
+    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut gpui::MutableAppContext) {
+        self.pane_focused = pane_focused;
+    }
 }

crates/editor/src/display_map.rs πŸ”—

@@ -19,7 +19,7 @@ use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
 pub use suggestion_map::Suggestion;
 use suggestion_map::SuggestionMap;
 use sum_tree::{Bias, TreeMap};
-use tab_map::{TabMap, TabSnapshot};
+use tab_map::TabMap;
 use wrap_map::WrapMap;
 
 pub use block_map::{
@@ -643,25 +643,21 @@ impl DisplaySnapshot {
             .buffer_snapshot
             .buffer_line_for_row(buffer_row)
             .unwrap();
-        let chars = buffer.chars_at(Point::new(range.start.row, 0));
 
+        let mut indent_size = 0;
         let mut is_blank = false;
-        let indent_size = TabSnapshot::expand_tabs(
-            chars.take_while(|c| {
-                if *c == ' ' || *c == '\t' {
-                    true
-                } else {
-                    if *c == '\n' {
-                        is_blank = true;
-                    }
-                    false
+        for c in buffer.chars_at(Point::new(range.start.row, 0)) {
+            if c == ' ' || c == '\t' {
+                indent_size += 1;
+            } else {
+                if c == '\n' {
+                    is_blank = true;
                 }
-            }),
-            buffer.line_len(buffer_row) as usize, // Never collapse
-            self.tab_snapshot.tab_size,
-        );
+                break;
+            }
+        }
 
-        (indent_size as u32, is_blank)
+        (indent_size, is_blank)
     }
 
     pub fn line_len(&self, row: u32) -> u32 {

crates/editor/src/display_map/suggestion_map.rs πŸ”—

@@ -208,6 +208,32 @@ impl SuggestionSnapshot {
         }
     }
 
+    pub fn line_len(&self, row: u32) -> u32 {
+        if let Some(suggestion) = &self.suggestion {
+            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
+            let suggestion_end = suggestion_start + suggestion.text.max_point();
+
+            if row < suggestion_start.row {
+                self.fold_snapshot.line_len(row)
+            } else if row > suggestion_end.row {
+                self.fold_snapshot
+                    .line_len(suggestion_start.row + (row - suggestion_end.row))
+            } else {
+                let mut result = suggestion.text.line_len(row - suggestion_start.row);
+                if row == suggestion_start.row {
+                    result += suggestion_start.column;
+                }
+                if row == suggestion_end.row {
+                    result +=
+                        self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
+                }
+                result
+            }
+        } else {
+            self.fold_snapshot.line_len(row)
+        }
+    }
+
     pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
         if let Some(suggestion) = self.suggestion.as_ref() {
             let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;

crates/editor/src/display_map/tab_map.rs πŸ”—

@@ -9,6 +9,8 @@ use parking_lot::Mutex;
 use std::{cmp, mem, num::NonZeroU32, ops::Range};
 use sum_tree::Bias;
 
+const MAX_EXPANSION_COLUMN: u32 = 256;
+
 pub struct TabMap(Mutex<TabSnapshot>);
 
 impl TabMap {
@@ -16,11 +18,18 @@ impl TabMap {
         let snapshot = TabSnapshot {
             suggestion_snapshot: input,
             tab_size,
+            max_expansion_column: MAX_EXPANSION_COLUMN,
             version: 0,
         };
         (Self(Mutex::new(snapshot.clone())), snapshot)
     }
 
+    #[cfg(test)]
+    pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot {
+        self.0.lock().max_expansion_column = column;
+        self.0.lock().clone()
+    }
+
     pub fn sync(
         &self,
         suggestion_snapshot: SuggestionSnapshot,
@@ -31,6 +40,7 @@ impl TabMap {
         let mut new_snapshot = TabSnapshot {
             suggestion_snapshot,
             tab_size,
+            max_expansion_column: old_snapshot.max_expansion_column,
             version: old_snapshot.version,
         };
 
@@ -42,28 +52,66 @@ impl TabMap {
         let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
 
         if old_snapshot.tab_size == new_snapshot.tab_size {
+            // Expand each edit to include the next tab on the same line as the edit,
+            // and any subsequent tabs on that line that moved across the tab expansion
+            // boundary.
             for suggestion_edit in &mut suggestion_edits {
-                let mut delta = 0;
-                for chunk in old_snapshot.suggestion_snapshot.chunks(
+                let old_end_column = old_snapshot
+                    .suggestion_snapshot
+                    .to_point(suggestion_edit.old.end)
+                    .column();
+                let new_end_column = new_snapshot
+                    .suggestion_snapshot
+                    .to_point(suggestion_edit.new.end)
+                    .column();
+
+                let mut offset_from_edit = 0;
+                let mut first_tab_offset = None;
+                let mut last_tab_with_changed_expansion_offset = None;
+                'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
                     suggestion_edit.old.end..old_max_offset,
                     false,
                     None,
                     None,
                 ) {
-                    let patterns: &[_] = &['\t', '\n'];
-                    if let Some(ix) = chunk.text.find(patterns) {
-                        if &chunk.text[ix..ix + 1] == "\t" {
-                            suggestion_edit.old.end.0 += delta + ix + 1;
-                            suggestion_edit.new.end.0 += delta + ix + 1;
+                    for (ix, mat) in chunk.text.match_indices(&['\t', '\n']) {
+                        let offset_from_edit = offset_from_edit + (ix as u32);
+                        match mat {
+                            "\t" => {
+                                if first_tab_offset.is_none() {
+                                    first_tab_offset = Some(offset_from_edit);
+                                }
+
+                                let old_column = old_end_column + offset_from_edit;
+                                let new_column = new_end_column + offset_from_edit;
+                                let was_expanded = old_column < old_snapshot.max_expansion_column;
+                                let is_expanded = new_column < new_snapshot.max_expansion_column;
+                                if was_expanded != is_expanded {
+                                    last_tab_with_changed_expansion_offset = Some(offset_from_edit);
+                                } else if !was_expanded && !is_expanded {
+                                    break 'outer;
+                                }
+                            }
+                            "\n" => break 'outer,
+                            _ => unreachable!(),
                         }
+                    }
 
+                    offset_from_edit += chunk.text.len() as u32;
+                    if old_end_column + offset_from_edit >= old_snapshot.max_expansion_column
+                        && new_end_column | offset_from_edit >= new_snapshot.max_expansion_column
+                    {
                         break;
                     }
+                }
 
-                    delta += chunk.text.len();
+                if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
+                    suggestion_edit.old.end.0 += offset as usize + 1;
+                    suggestion_edit.new.end.0 += offset as usize + 1;
                 }
             }
 
+            // Combine any edits that overlap due to the expansion.
             let mut ix = 1;
             while ix < suggestion_edits.len() {
                 let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
@@ -113,6 +161,7 @@ impl TabMap {
 pub struct TabSnapshot {
     pub suggestion_snapshot: SuggestionSnapshot,
     pub tab_size: NonZeroU32,
+    pub max_expansion_column: u32,
     pub version: usize,
 }
 
@@ -124,15 +173,12 @@ impl TabSnapshot {
     pub fn line_len(&self, row: u32) -> u32 {
         let max_point = self.max_point();
         if row < max_point.row() {
-            self.chunks(
-                TabPoint::new(row, 0)..TabPoint::new(row + 1, 0),
-                false,
-                None,
-                None,
-            )
-            .map(|chunk| chunk.text.len() as u32)
-            .sum::<u32>()
-                - 1
+            self.to_tab_point(SuggestionPoint::new(
+                row,
+                self.suggestion_snapshot.line_len(row),
+            ))
+            .0
+            .column
         } else {
             max_point.column()
         }
@@ -200,12 +246,13 @@ impl TabSnapshot {
     ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
             self.to_suggestion_point(range.start, Bias::Left);
+        let input_column = input_start.column();
         let input_start = self.suggestion_snapshot.to_offset(input_start);
         let input_end = self
             .suggestion_snapshot
             .to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
-        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 {
-            (range.end.column() - range.start.column()) as usize
+        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
+            range.end.column() - range.start.column()
         } else {
             to_next_stop
         };
@@ -217,15 +264,17 @@ impl TabSnapshot {
                 text_highlights,
                 suggestion_highlight,
             ),
+            input_column,
             column: expanded_char_column,
+            max_expansion_column: self.max_expansion_column,
             output_position: range.start.0,
             max_output_position: range.end.0,
             tab_size: self.tab_size,
             chunk: Chunk {
-                text: &SPACES[0..to_next_stop],
+                text: &SPACES[0..(to_next_stop as usize)],
                 ..Default::default()
             },
-            skip_leading_tab: to_next_stop > 0,
+            inside_leading_tab: to_next_stop > 0,
         }
     }
 
@@ -255,21 +304,17 @@ impl TabSnapshot {
         let chars = self
             .suggestion_snapshot
             .chars_at(SuggestionPoint::new(input.row(), 0));
-        let expanded = Self::expand_tabs(chars, input.column() as usize, self.tab_size);
-        TabPoint::new(input.row(), expanded as u32)
+        let expanded = self.expand_tabs(chars, input.column());
+        TabPoint::new(input.row(), expanded)
     }
 
-    pub fn to_suggestion_point(
-        &self,
-        output: TabPoint,
-        bias: Bias,
-    ) -> (SuggestionPoint, usize, usize) {
+    pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) {
         let chars = self
             .suggestion_snapshot
             .chars_at(SuggestionPoint::new(output.row(), 0));
-        let expanded = output.column() as usize;
+        let expanded = output.column();
         let (collapsed, expanded_char_column, to_next_stop) =
-            Self::collapse_tabs(chars, expanded, bias, self.tab_size);
+            self.collapse_tabs(chars, expanded, bias);
         (
             SuggestionPoint::new(output.row(), collapsed as u32),
             expanded_char_column,
@@ -292,38 +337,38 @@ impl TabSnapshot {
         fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
     }
 
-    pub fn expand_tabs(
-        chars: impl Iterator<Item = char>,
-        column: usize,
-        tab_size: NonZeroU32,
-    ) -> usize {
+    fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
+        let tab_size = self.tab_size.get();
+
         let mut expanded_chars = 0;
         let mut expanded_bytes = 0;
         let mut collapsed_bytes = 0;
+        let end_column = column.min(self.max_expansion_column);
         for c in chars {
-            if collapsed_bytes == column {
+            if collapsed_bytes >= end_column {
                 break;
             }
             if c == '\t' {
-                let tab_size = tab_size.get() as usize;
                 let tab_len = tab_size - expanded_chars % tab_size;
                 expanded_bytes += tab_len;
                 expanded_chars += tab_len;
             } else {
-                expanded_bytes += c.len_utf8();
+                expanded_bytes += c.len_utf8() as u32;
                 expanded_chars += 1;
             }
-            collapsed_bytes += c.len_utf8();
+            collapsed_bytes += c.len_utf8() as u32;
         }
-        expanded_bytes
+        expanded_bytes + column.saturating_sub(collapsed_bytes)
     }
 
     fn collapse_tabs(
+        &self,
         chars: impl Iterator<Item = char>,
-        column: usize,
+        column: u32,
         bias: Bias,
-        tab_size: NonZeroU32,
-    ) -> (usize, usize, usize) {
+    ) -> (u32, u32, u32) {
+        let tab_size = self.tab_size.get();
+
         let mut expanded_bytes = 0;
         let mut expanded_chars = 0;
         let mut collapsed_bytes = 0;
@@ -331,9 +376,11 @@ impl TabSnapshot {
             if expanded_bytes >= column {
                 break;
             }
+            if collapsed_bytes >= self.max_expansion_column {
+                break;
+            }
 
             if c == '\t' {
-                let tab_size = tab_size.get() as usize;
                 let tab_len = tab_size - (expanded_chars % tab_size);
                 expanded_chars += tab_len;
                 expanded_bytes += tab_len;
@@ -346,7 +393,7 @@ impl TabSnapshot {
                 }
             } else {
                 expanded_chars += 1;
-                expanded_bytes += c.len_utf8();
+                expanded_bytes += c.len_utf8() as u32;
             }
 
             if expanded_bytes > column && matches!(bias, Bias::Left) {
@@ -354,9 +401,13 @@ impl TabSnapshot {
                 break;
             }
 
-            collapsed_bytes += c.len_utf8();
+            collapsed_bytes += c.len_utf8() as u32;
         }
-        (collapsed_bytes, expanded_chars, 0)
+        (
+            collapsed_bytes + column.saturating_sub(expanded_bytes),
+            expanded_chars,
+            0,
+        )
     }
 }
 
@@ -444,11 +495,13 @@ const SPACES: &str = "                ";
 pub struct TabChunks<'a> {
     suggestion_chunks: SuggestionChunks<'a>,
     chunk: Chunk<'a>,
-    column: usize,
+    column: u32,
+    max_expansion_column: u32,
     output_position: Point,
+    input_column: u32,
     max_output_position: Point,
     tab_size: NonZeroU32,
-    skip_leading_tab: bool,
+    inside_leading_tab: bool,
 }
 
 impl<'a> Iterator for TabChunks<'a> {
@@ -458,9 +511,10 @@ impl<'a> Iterator for TabChunks<'a> {
         if self.chunk.text.is_empty() {
             if let Some(chunk) = self.suggestion_chunks.next() {
                 self.chunk = chunk;
-                if self.skip_leading_tab {
+                if self.inside_leading_tab {
                     self.chunk.text = &self.chunk.text[1..];
-                    self.skip_leading_tab = false;
+                    self.inside_leading_tab = false;
+                    self.input_column += 1;
                 }
             } else {
                 return None;
@@ -479,27 +533,36 @@ impl<'a> Iterator for TabChunks<'a> {
                         });
                     } else {
                         self.chunk.text = &self.chunk.text[1..];
-                        let tab_size = self.tab_size.get() as u32;
-                        let mut len = tab_size - self.column as u32 % tab_size;
+                        let tab_size = if self.input_column < self.max_expansion_column {
+                            self.tab_size.get() as u32
+                        } else {
+                            1
+                        };
+                        let mut len = tab_size - self.column % tab_size;
                         let next_output_position = cmp::min(
                             self.output_position + Point::new(0, len),
                             self.max_output_position,
                         );
                         len = next_output_position.column - self.output_position.column;
-                        self.column += len as usize;
+                        self.column += len;
+                        self.input_column += 1;
                         self.output_position = next_output_position;
                         return Some(Chunk {
-                            text: &SPACES[0..len as usize],
+                            text: &SPACES[..len as usize],
                             ..self.chunk
                         });
                     }
                 }
                 '\n' => {
                     self.column = 0;
+                    self.input_column = 0;
                     self.output_position += Point::new(1, 0);
                 }
                 _ => {
                     self.column += 1;
+                    if !self.inside_leading_tab {
+                        self.input_column += c.len_utf8() as u32;
+                    }
                     self.output_position.column += c.len_utf8() as u32;
                 }
             }
@@ -518,20 +581,83 @@ mod tests {
     };
     use rand::{prelude::StdRng, Rng};
 
-    #[test]
-    fn test_expand_tabs() {
-        assert_eq!(
-            TabSnapshot::expand_tabs("\t".chars(), 0, 4.try_into().unwrap()),
-            0
-        );
-        assert_eq!(
-            TabSnapshot::expand_tabs("\t".chars(), 1, 4.try_into().unwrap()),
-            4
-        );
-        assert_eq!(
-            TabSnapshot::expand_tabs("\ta".chars(), 2, 4.try_into().unwrap()),
-            5
-        );
+    #[gpui::test]
+    fn test_expand_tabs(cx: &mut gpui::MutableAppContext) {
+        let buffer = MultiBuffer::build_simple("", cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+
+        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
+        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
+        assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
+    }
+
+    #[gpui::test]
+    fn test_long_lines(cx: &mut gpui::MutableAppContext) {
+        let max_expansion_column = 12;
+        let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
+        let output = "A   BC  DEF G   HI J K L M";
+
+        let buffer = MultiBuffer::build_simple(input, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+
+        tab_snapshot.max_expansion_column = max_expansion_column;
+        assert_eq!(tab_snapshot.text(), output);
+
+        for (ix, c) in input.char_indices() {
+            assert_eq!(
+                tab_snapshot
+                    .chunks(
+                        TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
+                        false,
+                        None,
+                        None,
+                    )
+                    .map(|c| c.text)
+                    .collect::<String>(),
+                &output[ix..],
+                "text from index {ix}"
+            );
+
+            if c != '\t' {
+                let input_point = Point::new(0, ix as u32);
+                let output_point = Point::new(0, output.find(c).unwrap() as u32);
+                assert_eq!(
+                    tab_snapshot.to_tab_point(SuggestionPoint(input_point)),
+                    TabPoint(output_point),
+                    "to_tab_point({input_point:?})"
+                );
+                assert_eq!(
+                    tab_snapshot
+                        .to_suggestion_point(TabPoint(output_point), Bias::Left)
+                        .0,
+                    SuggestionPoint(input_point),
+                    "to_suggestion_point({output_point:?})"
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    fn test_long_lines_with_character_spanning_max_expansion_column(
+        cx: &mut gpui::MutableAppContext,
+    ) {
+        let max_expansion_column = 8;
+        let input = "abcdefgβ‹―hij";
+
+        let buffer = MultiBuffer::build_simple(input, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+
+        tab_snapshot.max_expansion_column = max_expansion_column;
+        assert_eq!(tab_snapshot.text(), input);
     }
 
     #[gpui::test(iterations = 100)]
@@ -557,7 +683,9 @@ mod tests {
         let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
         log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
 
-        let (_, tabs_snapshot) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let tabs_snapshot = tab_map.set_max_expansion_column(32);
+
         let text = text::Rope::from(tabs_snapshot.text().as_str());
         log::info!(
             "TabMap text (tab size: {}): {:?}",
@@ -582,11 +710,11 @@ mod tests {
                 .collect::<String>();
             let expected_summary = TextSummary::from(expected_text.as_str());
             assert_eq!(
-                expected_text,
                 tabs_snapshot
                     .chunks(start..end, false, None, None)
                     .map(|c| c.text)
                     .collect::<String>(),
+                expected_text,
                 "chunks({:?}..{:?})",
                 start,
                 end
@@ -601,7 +729,11 @@ mod tests {
         }
 
         for row in 0..=text.max_point().row {
-            assert_eq!(tabs_snapshot.line_len(row), text.line_len(row));
+            assert_eq!(
+                tabs_snapshot.line_len(row),
+                text.line_len(row),
+                "line_len({row})"
+            );
         }
     }
 }

crates/editor/src/display_map/wrap_map.rs πŸ”—

@@ -1093,7 +1093,8 @@ mod tests {
         log::info!("FoldMap text: {:?}", fold_snapshot.text());
         let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
         log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
-        let (tab_map, tabs_snapshot) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let tabs_snapshot = tab_map.set_max_expansion_column(32);
         log::info!("TabMap text: {:?}", tabs_snapshot.text());
 
         let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);

crates/editor/src/editor_tests.rs πŸ”—

@@ -630,7 +630,7 @@ fn test_cancel(cx: &mut gpui::MutableAppContext) {
 }
 
 #[gpui::test]
-fn test_fold(cx: &mut gpui::MutableAppContext) {
+fn test_fold_action(cx: &mut gpui::MutableAppContext) {
     cx.set_global(Settings::test(cx));
     let buffer = MultiBuffer::build_simple(
         &"

crates/editor/src/items.rs πŸ”—

@@ -747,11 +747,15 @@ impl Item for Editor {
             .map(|path| path.to_string_lossy().to_string())
             .unwrap_or_else(|| "untitled".to_string());
 
-        let mut breadcrumbs = vec![Label::new(filename, theme.breadcrumbs.text.clone()).boxed()];
+        let filename_label = Label::new(filename, theme.workspace.breadcrumbs.default.text.clone());
+        let mut breadcrumbs = vec![filename_label.boxed()];
         breadcrumbs.extend(symbols.into_iter().map(|symbol| {
-            Text::new(symbol.text, theme.breadcrumbs.text.clone())
-                .with_highlights(symbol.highlight_ranges)
-                .boxed()
+            Text::new(
+                symbol.text,
+                theme.workspace.breadcrumbs.default.text.clone(),
+            )
+            .with_highlights(symbol.highlight_ranges)
+            .boxed()
         }));
         Some(breadcrumbs)
     }

crates/language/src/language.rs πŸ”—

@@ -78,12 +78,6 @@ pub trait ToLspPosition {
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub struct LanguageServerName(pub Arc<str>);
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
-pub enum ServerExecutionKind {
-    Launch,
-    Node,
-}
-
 #[derive(Debug, Clone, Deserialize)]
 pub struct LanguageServerBinary {
     pub path: PathBuf,

crates/pando/Cargo.toml πŸ”—

@@ -1,21 +0,0 @@
-[package]
-name = "pando"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/pando.rs"
-
-[features]
-test-support = []
-
-[dependencies]
-anyhow = "1.0.38"
-client = { path = "../client" }
-gpui = { path = "../gpui" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
-sqlez = { path = "../sqlez" }
-sqlez_macros = { path = "../sqlez_macros" }

crates/pando/src/pando.rs πŸ”—

@@ -1,15 +0,0 @@
-//! ## Goals
-//! - Opinionated Subset of Obsidian. Only the things that cant be done other ways in zed
-//! - Checked in .zp file is an sqlite db containing graph metadata
-//! - All nodes are file urls
-//! - Markdown links auto add soft linked nodes to the db
-//! - Links create positioning data regardless of if theres a file
-//! - Lock links to make structure that doesn't rotate or spread
-//! - Drag from file finder to pando item to add it in
-//! - For linked files, zoom out to see closest linking pando file
-
-//! ## Plan
-//! - [ ] Make item backed by .zp sqlite file with camera position by user account
-//! - [ ] Render grid of dots and allow scrolling around the grid
-//! - [ ] Add scale property to layer canvas and manipulate it with pinch zooming
-//! - [ ] Allow dropping files onto .zp pane. Their relative path is recorded into the file along with

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -612,7 +612,7 @@ impl Item for TerminalView {
     fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
         Some(vec![Text::new(
             self.terminal().read(cx).breadcrumb_text.clone(),
-            theme.breadcrumbs.text.clone(),
+            theme.workspace.breadcrumbs.default.text.clone(),
         )
         .boxed()])
     }

crates/theme/src/theme.rs πŸ”—

@@ -31,7 +31,6 @@ pub struct Theme {
     pub editor: Editor,
     pub search: Search,
     pub project_diagnostics: ProjectDiagnostics,
-    pub breadcrumbs: ContainedText,
     pub shared_screen: ContainerStyle,
     pub contact_notification: ContactNotification,
     pub update_notification: UpdateNotification,
@@ -63,6 +62,8 @@ pub struct Workspace {
     pub sidebar: Sidebar,
     pub status_bar: StatusBar,
     pub toolbar: Toolbar,
+    pub breadcrumb_height: f32,
+    pub breadcrumbs: Interactive<ContainedText>,
     pub disconnected_overlay: ContainedText,
     pub modal: ContainerStyle,
     pub notification: ContainerStyle,

crates/workspace/src/pane.rs πŸ”—

@@ -1603,6 +1603,10 @@ impl View for Pane {
     }
 
     fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.pane_focus_update(true, cx);
+        });
+
         if let Some(active_item) = self.active_item() {
             if cx.is_self_focused() {
                 // Pane was focused directly. We need to either focus a view inside the active item,
@@ -1626,6 +1630,12 @@ impl View for Pane {
         }
     }
 
+    fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.pane_focus_update(false, cx);
+        });
+    }
+
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut keymap = Self::default_keymap_context();
         if self.docked.is_some() {

crates/workspace/src/toolbar.rs πŸ”—

@@ -20,6 +20,8 @@ pub trait ToolbarItemView: View {
     ) -> ToolbarItemLocation {
         current_location
     }
+
+    fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut MutableAppContext) {}
 }
 
 trait ToolbarItemViewHandle {
@@ -30,6 +32,7 @@ trait ToolbarItemViewHandle {
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut MutableAppContext,
     ) -> ToolbarItemLocation;
+    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext);
 }
 
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -260,6 +263,12 @@ impl Toolbar {
         }
     }
 
+    pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext) {
+        for (toolbar_item, _) in self.items.iter_mut() {
+            toolbar_item.pane_focus_update(pane_focused, cx);
+        }
+    }
+
     pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
         self.items
             .iter()
@@ -289,6 +298,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
             this.set_active_pane_item(active_pane_item, cx)
         })
     }
+
+    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.pane_focus_update(pane_focused, cx));
+    }
 }
 
 impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {

crates/zed/Cargo.toml πŸ”—

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.80.0"
+version = "0.81.0"
 publish = false
 
 [lib]

styles/src/styleTree/app.ts πŸ”—

@@ -46,12 +46,6 @@ export default function app(colorScheme: ColorScheme): Object {
         contactList: contactList(colorScheme),
         search: search(colorScheme),
         sharedScreen: sharedScreen(colorScheme),
-        breadcrumbs: {
-            ...text(colorScheme.highest, "sans", "variant"),
-            padding: {
-                left: 6,
-            },
-        },
         updateNotification: updateNotification(colorScheme),
         simpleMessageNotification: simpleMessageNotification(colorScheme),
         tooltip: tooltip(colorScheme),

styles/src/styleTree/workspace.ts πŸ”—

@@ -263,9 +263,22 @@ export default function workspace(colorScheme: ColorScheme) {
             },
             padding: { left: 8, right: 8, top: 4, bottom: 4 },
         },
+        breadcrumbHeight: 24,
         breadcrumbs: {
-            ...text(layer, "mono", "variant"),
-            padding: { left: 6 },
+            ...text(colorScheme.highest, "sans", "variant"),
+            cornerRadius: 6,
+            padding: {
+                left: 6,
+                right: 6,
+            },
+            hover: {
+                color: foreground(colorScheme.highest, "on", "hovered"),
+                background: background(
+                    colorScheme.highest,
+                    "on",
+                    "hovered"
+                ),
+            },
         },
         disconnectedOverlay: {
             ...text(layer, "sans"),