Detailed changes
@@ -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<usize>,
+ style: &theme::Tab,
+ cx: &AppContext,
+ ) -> ElementBox {
render_summary(
&self.summary,
&style.label.text,
@@ -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)
}
@@ -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<Cow<'a, str>> {
+ 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<usize>,
+ 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<ProjectPath> {
@@ -534,3 +571,38 @@ impl StatusItemView for CursorPosition {
cx.notify();
}
}
+
+fn path_for_buffer<'a>(
+ buffer: &ModelHandle<MultiBuffer>,
+ mut depth: usize,
+ include_filename: bool,
+ cx: &'a AppContext,
+) -> Option<Cow<'a, Path>> {
+ 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())
+ }
+}
@@ -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();
}
}
@@ -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;
@@ -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 {
@@ -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<usize>,
+ tab_theme: &theme::Tab,
+ cx: &gpui::AppContext,
+ ) -> ElementBox {
let settings = cx.global::<Settings>();
let search_theme = &settings.theme.search;
Flex::row()
@@ -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<usize>,
+ tab_theme: &theme::Tab,
+ cx: &gpui::AppContext,
+ ) -> ElementBox {
let settings = cx.global::<Settings>();
let search_theme = &settings.theme.search; //TODO properly integrate themes
@@ -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,
@@ -840,8 +840,10 @@ impl Pane {
} else {
None
};
+
let mut row = Flex::row().scrollable::<Tabs, _>(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<usize> {
+ let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
+
+ 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 {
@@ -256,7 +256,11 @@ pub trait Item: View {
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
false
}
- fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
+ fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+ None
+ }
+ fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
+ -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
fn is_singleton(&self, cx: &AppContext) -> bool;
@@ -409,7 +413,9 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
}
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<Cow<'a, str>>;
+ fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
+ -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
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<T: Item> ItemHandle for ViewHandle<T> {
- 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<Cow<'a, str>> {
+ self.read(cx).tab_description(detail, cx)
+ }
+
+ fn tab_content(
+ &self,
+ detail: Option<usize>,
+ style: &theme::Tab,
+ cx: &AppContext,
+ ) -> ElementBox {
+ self.read(cx).tab_content(detail, style, cx)
}
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
@@ -3277,7 +3292,7 @@ mod tests {
}
impl Item for TestItem {
- fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
+ fn tab_content(&self, _: Option<usize>, _: &theme::Tab, _: &AppContext) -> ElementBox {
Empty::new().boxed()
}
@@ -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 = {