project_panel: Show path tooltip on hover (#63299)
# Objective
The project panel doesn't show a file or folder's actual location on
disk anywhere in the UI — you have to rely on the tab title or context
menu ("Copy Path") to find out where a file actually lives, which is
especially unclear for files with the same name in different folders, or
when a project has multiple worktrees.
## Solution
- Adds a tooltip to each file/folder row in the project panel that shows
the entry's path on hover, with the home directory abbreviated to `~`
(via the existing `PathExt::compact()` utility, consistent with how
paths are already displayed elsewhere, e.g. the Recent Projects list).
## Testing
- Manually tested by hovering over files and folders at various nesting
depths in the project panel and confirming the tooltip shows the correct
path.
- Tested with a project both inside and outside the home directory to
confirm the `~` substitution only applies when appropriate.
- Confirmed existing row behavior (selection, click-to-open,
drag-and-drop, right-click context menu) is unaffected by the added
tooltip.
- Reviewers: hover over any row in the project panel and confirm the
tooltip appears after the normal hover delay and shows the expected
path.
- Tested on macOS only — haven't verified on Linux/Windows.
## Self-Review Checklist:
- [ ] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable
## Showcase
<img width="1512" height="871" alt="image"
src="https://github.com/user-attachments/assets/ec86f9ab-0a8e-40f7-b783-8a115203f11d"
/>
- Help others understand the result of this PR by showcasing your
awesome work!
- If this PR includes a visual change, consider adding a screenshot,
GIF, or video
- A before/after comparison is very useful for changes to existing
features!
While a showcase should aim to be brief and digestible, you can use a
toggleable section to save space on longer showcases:
<details>
<summary>Click to view showcase</summary>
My super cool demos here
</details>
---
Release Notes:
- Added a tooltip showing the file path when hovering over files and
folders in the project panel.
---------
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
1 parent
c1eda3e88a
commit
9c3e2a071f
7 files changed
+166
-5
No files matched your search
@@ -843,6 +843,8 @@
|
||||
"button": true,
|
||||
// Whether to hide the gitignore entries in the project panel.
|
||||
"hide_gitignore": false,
|
||||
// Delay in milliseconds before tooltips appear for project panel titles.
|
||||
"title_tooltip_delay": "default",
|
||||
// Default width of the project panel.
|
||||
"default_width": 240,
|
||||
// Where to dock the project panel. Can be 'left' or 'right'.
|
||||
|
||||
@@ -52,8 +52,7 @@ use std::{
|
||||
cell::OnceCell,
|
||||
cmp,
|
||||
collections::HashSet,
|
||||
ops::Neg,
|
||||
ops::Range,
|
||||
ops::{Neg, Range},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -68,7 +67,7 @@ use util::{
|
||||
ResultExt, TakeUntilExt, TryFutureExt,
|
||||
markdown::MarkdownInlineCode,
|
||||
maybe,
|
||||
paths::{PathStyle, compare_paths},
|
||||
paths::{PathExt, PathStyle, compare_paths},
|
||||
rel_path::{RelPath, RelPathBuf},
|
||||
};
|
||||
use workspace::{
|
||||
@@ -5875,10 +5874,39 @@ impl ProjectPanel {
|
||||
(entry_id.to_proto() as usize).into()
|
||||
};
|
||||
|
||||
let tooltip_path = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.map(|worktree| {
|
||||
worktree
|
||||
.read(cx)
|
||||
.absolutize(&path)
|
||||
.compact()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
});
|
||||
|
||||
div()
|
||||
.id(id.clone())
|
||||
.relative()
|
||||
.group(GROUP_NAME)
|
||||
.when_some(
|
||||
tooltip_path.and_then(|path| {
|
||||
let delay = match settings.title_tooltip_delay {
|
||||
settings::ProjectPanelTitleTooltipDelay::Default => 1500,
|
||||
settings::ProjectPanelTitleTooltipDelay::Custom(delay_ms) => delay_ms.0,
|
||||
settings::ProjectPanelTitleTooltipDelay::Disabled => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some((path, delay))
|
||||
}),
|
||||
|this, (path, delay)| {
|
||||
this.tooltip_show_delay(Duration::from_millis(delay))
|
||||
.tooltip(Tooltip::text(path))
|
||||
},
|
||||
)
|
||||
.cursor_pointer()
|
||||
.rounded_none()
|
||||
.bg(bg_color)
|
||||
|
||||
@@ -4,7 +4,8 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
DockSide, FolderIndicator, IntoGpui, ProjectPanelEntrySpacing, ProjectPanelSortMode,
|
||||
ProjectPanelSortOrder, RegisterSetting, Settings, ShowDiagnostics, ShowIndentGuides,
|
||||
ProjectPanelSortOrder, ProjectPanelTitleTooltipDelay, RegisterSetting, Settings,
|
||||
ShowDiagnostics, ShowIndentGuides,
|
||||
};
|
||||
use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
|
||||
|
||||
@@ -13,6 +14,7 @@ pub struct ProjectPanelSettings {
|
||||
pub button: bool,
|
||||
pub hide_gitignore: bool,
|
||||
pub default_width: Pixels,
|
||||
pub title_tooltip_delay: ProjectPanelTitleTooltipDelay,
|
||||
pub dock: DockSide,
|
||||
pub entry_spacing: ProjectPanelEntrySpacing,
|
||||
pub file_icons: bool,
|
||||
@@ -99,6 +101,7 @@ impl Settings for ProjectPanelSettings {
|
||||
hide_gitignore: project_panel.hide_gitignore.unwrap(),
|
||||
default_width: project_panel.default_width.unwrap().into_gpui(),
|
||||
dock: project_panel.dock.unwrap(),
|
||||
title_tooltip_delay: project_panel.title_tooltip_delay.unwrap(),
|
||||
entry_spacing: project_panel.entry_spacing.unwrap(),
|
||||
file_icons: project_panel.file_icons.unwrap(),
|
||||
folder_indicator: project_panel.folder_indicator.unwrap(),
|
||||
|
||||
@@ -827,6 +827,7 @@ impl VsCodeSettings {
|
||||
auto_reveal_entries: self.read_bool("explorer.autoReveal"),
|
||||
bold_folder_labels: None,
|
||||
button: None,
|
||||
title_tooltip_delay: None,
|
||||
default_width: None,
|
||||
dock: None,
|
||||
drag_and_drop: None,
|
||||
|
||||
@@ -808,6 +808,8 @@ pub struct ProjectPanelSettingsContent {
|
||||
///
|
||||
/// Default: right (Agentic layout), left (Classic layout)
|
||||
pub dock: Option<DockSide>,
|
||||
// TODO
|
||||
pub title_tooltip_delay: Option<ProjectPanelTitleTooltipDelay>,
|
||||
/// Spacing between worktree entries in the project panel.
|
||||
///
|
||||
/// Default: comfortable
|
||||
@@ -893,6 +895,31 @@ pub struct ProjectPanelSettingsContent {
|
||||
pub git_status_indicator: Option<bool>,
|
||||
}
|
||||
|
||||
/// Controls the width of the git diff hunk indicators in the gutter.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
PartialEq,
|
||||
strum::EnumDiscriminants,
|
||||
)]
|
||||
#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectPanelTitleTooltipDelay {
|
||||
/// Default is 1500ms.
|
||||
#[default]
|
||||
Default,
|
||||
/// A custom offset in milliseconds for the tooltip show delay.
|
||||
Custom(crate::DelayMs),
|
||||
/// Disables the tooltip
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
|
||||
@@ -5265,7 +5265,7 @@ fn window_and_layout_page() -> SettingsPage {
|
||||
}
|
||||
|
||||
fn panels_page() -> SettingsPage {
|
||||
fn project_panel_section() -> [SettingsPageItem; 29] {
|
||||
fn project_panel_section() -> [SettingsPageItem; 30] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("Project Panel"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
@@ -5305,6 +5305,105 @@ fn panels_page() -> SettingsPage {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::DynamicItem(DynamicItem {
|
||||
discriminant: SettingItem {
|
||||
title: "Project Panel Title Tooltips Delay",
|
||||
description: "Delay in milliseconds before tooltips appear for project panel titles.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("project_panel.title_tooltip_delay$"),
|
||||
pick: |settings_content| {
|
||||
Some(
|
||||
&dynamic_variants::<settings::ProjectPanelTitleTooltipDelay>()
|
||||
[settings_content
|
||||
.project_panel
|
||||
.as_ref()?
|
||||
.title_tooltip_delay
|
||||
.as_ref()?
|
||||
.discriminant()
|
||||
as usize],
|
||||
)
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
let project_panel =
|
||||
settings_content.project_panel.get_or_insert_default();
|
||||
project_panel.title_tooltip_delay = value.map(|value| match value {
|
||||
settings::ProjectPanelTitleTooltipDelayDiscriminants::Default => {
|
||||
settings::ProjectPanelTitleTooltipDelay::Default
|
||||
}
|
||||
settings::ProjectPanelTitleTooltipDelayDiscriminants::Disabled => {
|
||||
settings::ProjectPanelTitleTooltipDelay::Disabled
|
||||
}
|
||||
settings::ProjectPanelTitleTooltipDelayDiscriminants::Custom => {
|
||||
let delay = match project_panel.title_tooltip_delay {
|
||||
Some(settings::ProjectPanelTitleTooltipDelay::Custom(
|
||||
delay,
|
||||
)) => settings::DelayMs(delay.0),
|
||||
_ => settings::DelayMs(1500),
|
||||
};
|
||||
settings::ProjectPanelTitleTooltipDelay::Custom(delay)
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
},
|
||||
pick_discriminant: |settings_content| {
|
||||
Some(
|
||||
settings_content
|
||||
.project_panel
|
||||
.as_ref()?
|
||||
.title_tooltip_delay
|
||||
.as_ref()?
|
||||
.discriminant() as usize,
|
||||
)
|
||||
},
|
||||
fields: dynamic_variants::<settings::ProjectPanelTitleTooltipDelay>()
|
||||
.into_iter()
|
||||
.map(|variant| match variant {
|
||||
settings::ProjectPanelTitleTooltipDelayDiscriminants::Default => vec![],
|
||||
settings::ProjectPanelTitleTooltipDelayDiscriminants::Disabled => vec![],
|
||||
settings::ProjectPanelTitleTooltipDelayDiscriminants::Custom => {
|
||||
vec![SettingItem {
|
||||
files: USER,
|
||||
title: "Custom Delay",
|
||||
description: "Delay in milliseconds of the project panel title tooltips.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("project_panel.title_tooltip_delay"),
|
||||
pick: |settings_content| match settings_content
|
||||
.project_panel
|
||||
.as_ref()
|
||||
.and_then(|project_panel| {
|
||||
project_panel.title_tooltip_delay.as_ref()
|
||||
}) {
|
||||
Some(settings::ProjectPanelTitleTooltipDelay::Custom(
|
||||
value,
|
||||
)) => Some(value),
|
||||
_ => None,
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
let Some(value) = value else {
|
||||
return;
|
||||
};
|
||||
if let Some(
|
||||
settings::ProjectPanelTitleTooltipDelay::Custom(width),
|
||||
) = settings_content.project_panel.as_mut().and_then(
|
||||
|project_panel| {
|
||||
project_panel.title_tooltip_delay.as_mut()
|
||||
},
|
||||
) {
|
||||
*width = value;
|
||||
}
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
}]
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Hide .gitignore",
|
||||
description: "Whether to hide the gitignore entries in the project panel.",
|
||||
|
||||
@@ -656,6 +656,7 @@ fn init_renderers(cx: &mut App) {
|
||||
.add_basic_renderer::<settings::IconThemeName>(render_icon_theme_picker)
|
||||
.add_basic_renderer::<settings::BufferLineHeightDiscriminants>(render_dropdown)
|
||||
.add_basic_renderer::<settings::GitGutterWidthDiscriminants>(render_dropdown)
|
||||
.add_basic_renderer::<settings::ProjectPanelTitleTooltipDelayDiscriminants>(render_dropdown)
|
||||
.add_basic_renderer::<settings::AutosaveSettingDiscriminants>(render_dropdown)
|
||||
.add_basic_renderer::<settings::WorkingDirectoryDiscriminants>(render_dropdown)
|
||||
.add_basic_renderer::<settings::IncludeIgnoredContent>(render_dropdown)
|
||||
|
||||
Reference in new issue
Block a user