diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index ecc1b2df68..1e89ba0853 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -501,7 +501,12 @@ impl ProjectDiagnosticsEditor { } impl workspace::Item for ProjectDiagnosticsEditor { - fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { + fn tab_content( + &self, + _detail: Option, + style: &theme::Tab, + cx: &AppContext, + ) -> ElementBox { render_summary( &self.summary, &style.label.text, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6adefcf62a..b0373c0fcb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1073,7 +1073,7 @@ impl Editor { &self.buffer } - pub fn title(&self, cx: &AppContext) -> String { + pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { self.buffer().read(cx).title(cx) } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0e3aca1447..f703acdfdb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,4 +1,6 @@ -use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToPoint as _}; +use crate::{ + Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer, NavigationData, ToPoint as _, +}; use anyhow::{anyhow, Result}; use futures::FutureExt; use gpui::{ @@ -10,7 +12,12 @@ use project::{File, Project, ProjectEntryId, ProjectPath}; use rpc::proto::{self, update_view}; use settings::Settings; use smallvec::SmallVec; -use std::{fmt::Write, path::PathBuf, time::Duration}; +use std::{ + borrow::Cow, + fmt::Write, + path::{Path, PathBuf}, + time::Duration, +}; use text::{Point, Selection}; use util::TryFutureExt; use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView}; @@ -292,9 +299,39 @@ impl Item for Editor { } } - fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { - let title = self.title(cx); - Label::new(title, style.label.clone()).boxed() + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { + match path_for_buffer(&self.buffer, detail, true, cx)? { + Cow::Borrowed(path) => Some(path.to_string_lossy()), + Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()), + } + } + + fn tab_content( + &self, + detail: Option, + style: &theme::Tab, + cx: &AppContext, + ) -> ElementBox { + Flex::row() + .with_child( + Label::new(self.title(cx).into(), style.label.clone()) + .aligned() + .boxed(), + ) + .with_children(detail.and_then(|detail| { + let path = path_for_buffer(&self.buffer, detail, false, cx)?; + Some( + Label::new( + path.to_string_lossy().into(), + style.description.text.clone(), + ) + .contained() + .with_style(style.description.container) + .aligned() + .boxed(), + ) + })) + .boxed() } fn project_path(&self, cx: &AppContext) -> Option { @@ -534,3 +571,38 @@ impl StatusItemView for CursorPosition { cx.notify(); } } + +fn path_for_buffer<'a>( + buffer: &ModelHandle, + mut depth: usize, + include_filename: bool, + cx: &'a AppContext, +) -> Option> { + let file = buffer.read(cx).as_singleton()?.read(cx).file()?; + + let mut path = file.path().as_ref(); + depth += 1; + while depth > 0 { + if let Some(parent) = path.parent() { + path = parent; + depth -= 1; + } else { + break; + } + } + + if depth > 0 { + let full_path = file.full_path(cx); + if include_filename { + Some(full_path.into()) + } else { + Some(full_path.parent().unwrap().to_path_buf().into()) + } + } else { + let mut path = file.path().strip_prefix(path).unwrap(); + if !include_filename { + path = path.parent().unwrap(); + } + Some(path.into()) + } +} diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 5f069d223f..d5b85b0aee 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -14,6 +14,7 @@ use language::{ use settings::Settings; use smallvec::SmallVec; use std::{ + borrow::Cow, cell::{Ref, RefCell}, cmp, fmt, io, iter::{self, FromIterator}, @@ -1194,14 +1195,14 @@ impl MultiBuffer { .collect() } - pub fn title(&self, cx: &AppContext) -> String { - if let Some(title) = self.title.clone() { - return title; + pub fn title<'a>(&'a self, cx: &'a AppContext) -> Cow<'a, str> { + if let Some(title) = self.title.as_ref() { + return title.into(); } if let Some(buffer) = self.as_singleton() { if let Some(file) = buffer.read(cx).file() { - return file.file_name(cx).to_string_lossy().into(); + return file.file_name(cx).to_string_lossy(); } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0d56ac1979..ee24539287 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -20,7 +20,7 @@ use std::{ any::Any, cmp::{self, Ordering}, collections::{BTreeMap, HashMap}, - ffi::OsString, + ffi::OsStr, future::Future, iter::{self, Iterator, Peekable}, mem, @@ -185,7 +185,7 @@ pub trait File: Send + Sync { /// Returns the last component of this handle's absolute path. If this handle refers to the root /// of its worktree, then this method will return the name of the worktree itself. - fn file_name(&self, cx: &AppContext) -> OsString; + fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr; fn is_deleted(&self) -> bool; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index dbd4b443a1..cc972b9bcd 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1646,11 +1646,10 @@ impl language::File for File { /// Returns the last component of this handle's absolute path. If this handle refers to the root /// of its worktree, then this method will return the name of the worktree itself. - fn file_name(&self, cx: &AppContext) -> OsString { + fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr { self.path .file_name() - .map(|name| name.into()) - .unwrap_or_else(|| OsString::from(&self.worktree.read(cx).root_name)) + .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name)) } fn is_deleted(&self) -> bool { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e1acc6a771..5098222ae0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -220,7 +220,12 @@ impl Item for ProjectSearchView { .update(cx, |editor, cx| editor.deactivated(cx)); } - fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + fn tab_content( + &self, + _detail: Option, + tab_theme: &theme::Tab, + cx: &gpui::AppContext, + ) -> ElementBox { let settings = cx.global::(); let search_theme = &settings.theme.search; Flex::row() diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 12c092d6e6..71587ef135 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -324,7 +324,12 @@ impl View for Terminal { } impl Item for Terminal { - fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + fn tab_content( + &self, + _detail: Option, + tab_theme: &theme::Tab, + cx: &gpui::AppContext, + ) -> ElementBox { let settings = cx.global::(); let search_theme = &settings.theme.search; //TODO properly integrate themes diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 7936a9b6bb..2299bc3477 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -93,6 +93,7 @@ pub struct Tab { pub container: ContainerStyle, #[serde(flatten)] pub label: LabelStyle, + pub description: ContainedText, pub spacing: f32, pub icon_width: f32, pub icon_close: Color, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 87b6ea7547..6862cc0028 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -840,8 +840,10 @@ impl Pane { } else { None }; + let mut row = Flex::row().scrollable::(1, autoscroll, cx); - for (ix, item) in self.items.iter().enumerate() { + for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() { + let detail = if detail == 0 { None } else { Some(detail) }; let is_active = ix == self.active_item_index; row.add_child({ @@ -850,7 +852,7 @@ impl Pane { } else { theme.workspace.tab.clone() }; - let title = item.tab_content(&tab_style, cx); + let title = item.tab_content(detail, &tab_style, cx); let mut style = if is_active { theme.workspace.active_tab.clone() @@ -971,6 +973,43 @@ impl Pane { row.boxed() }) } + + fn tab_details(&self, cx: &AppContext) -> Vec { + let mut tab_details = (0..self.items.len()).map(|_| 0).collect::>(); + + let mut tab_descriptions = HashMap::default(); + let mut done = false; + while !done { + done = true; + + // Store item indices by their tab description. + for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() { + if let Some(description) = item.tab_description(*detail, cx) { + if *detail == 0 + || Some(&description) != item.tab_description(detail - 1, cx).as_ref() + { + tab_descriptions + .entry(description) + .or_insert(Vec::new()) + .push(ix); + } + } + } + + // If two or more items have the same tab description, increase their level + // of detail and try again. + for (_, item_ixs) in tab_descriptions.drain() { + if item_ixs.len() > 1 { + done = false; + for ix in item_ixs { + tab_details[ix] += 1; + } + } + } + } + + tab_details + } } impl Entity for Pane { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index da62fe7e54..d72704da01 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -256,7 +256,11 @@ pub trait Item: View { fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { false } - fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; + fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { + None + } + fn tab_content(&self, detail: Option, style: &theme::Tab, cx: &AppContext) + -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; fn is_singleton(&self, cx: &AppContext) -> bool; @@ -409,7 +413,9 @@ impl FollowableItemHandle for ViewHandle { } pub trait ItemHandle: 'static + fmt::Debug { - fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; + fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option>; + fn tab_content(&self, detail: Option, style: &theme::Tab, cx: &AppContext) + -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; fn is_singleton(&self, cx: &AppContext) -> bool; @@ -463,8 +469,17 @@ impl dyn ItemHandle { } impl ItemHandle for ViewHandle { - fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { - self.read(cx).tab_content(style, cx) + fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option> { + self.read(cx).tab_description(detail, cx) + } + + fn tab_content( + &self, + detail: Option, + style: &theme::Tab, + cx: &AppContext, + ) -> ElementBox { + self.read(cx).tab_content(detail, style, cx) } fn project_path(&self, cx: &AppContext) -> Option { @@ -3277,7 +3292,7 @@ mod tests { } impl Item for TestItem { - fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox { + fn tab_content(&self, _: Option, _: &theme::Tab, _: &AppContext) -> ElementBox { Empty::new().boxed() } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 36d47bed92..ef5b1fe69c 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -27,6 +27,10 @@ export default function workspace(theme: Theme) { left: 8, right: 8, }, + description: { + margin: { left: 6, top: 1 }, + ...text(theme, "sans", "muted", { size: "2xs" }) + } }; const activeTab = {