Add folder_indicator setting to show chevron and folder icon together (#62393)

# Objective

- Enable panels to show chevron and icon at the same time.
- Closes #8661

## Solution

- Replaces `folder_icon` with a new `folder_indicator` which converts
from a boolean to enum (`both`, `chevron`, `icon`) for the three display
cases

## Testing

- gpui and migration tests had been applied

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] 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)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

### project panel

both | chevron | icon
--- | --- | ---
<img width="400" alt="both_project_panel"
src="https://github.com/user-attachments/assets/4c21e1a1-a7a4-4471-b7de-fe867373ddbd"
/> | <img width="400" alt="chevron_project_panel"
src="https://github.com/user-attachments/assets/a626a82e-e5b7-470d-9d93-ed5787dc83d6"
/> | <img width="400" alt="icon_project_panel"
src="https://github.com/user-attachments/assets/20f9c55e-87c4-477a-b4c9-b070d6e5a1db"
/>

### git panel

both | chevron | icon
--- | --- | ---
<img width="400" alt="both_git_panel"
src="https://github.com/user-attachments/assets/3dc166dd-9e63-4943-b803-df3f167876b6"
/> | <img width="400" alt="chevron_git_panel"
src="https://github.com/user-attachments/assets/8dfd2c10-3a68-439a-8019-4951b72e17d1"
/> | <img width="400" alt="icon_git_panel"
src="https://github.com/user-attachments/assets/d78a9938-91e7-4d39-a3c8-04647da67768"
/>

### outline panel

both | chevron | icon
--- | --- | ---
<img width="400" alt="both_outline_panel"
src="https://github.com/user-attachments/assets/1b58c679-0bea-4600-a8ac-e800b0e8d24c"
/> | <img width="400" alt="chevron_outline_panel"
src="https://github.com/user-attachments/assets/0a9358e4-ec89-40d8-8d5e-a53b784ed30a"
/> | <img width="400" alt="icon_outline_panel"
src="https://github.com/user-attachments/assets/d8bf5199-5a8c-4de0-b516-00247e6531a5"
/>



---

Release Notes:

- Added a `folder_indicator` setting that can show the disclosure
chevron and folder icon at the same time in the project, outline, and
git panels.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
Dong 2026-08-27 20:11:28 +00:00 • committed by GitHub
parent 1e2e422c7b
commit 8166e3d7b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1119 additions and 173 deletions

1
Cargo.lock generated
View file

@ -6438,6 +6438,7 @@ name = "file_icons"
version = "0.1.0"
dependencies = [
"gpui",
"settings_content",
"theme",
"util",
]

View file

@ -830,8 +830,15 @@
"entry_spacing": "comfortable",
// Whether to show file icons in the project panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the project panel.
"folder_icons": true,
// What to show for directories in the project panel.
//
// 1. A folder icon:
// "icon"
// 2. A disclosure chevron:
// "chevron"
// 3. A disclosure chevron followed by a folder icon:
// "both"
"folder_indicator": "icon",
// Whether to show the git status in the project panel.
"git_status": true,
// Amount of indentation for nested items.
@ -945,8 +952,15 @@
"dock": "right",
// Whether to show file icons in the outline panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the outline panel.
"folder_icons": true,
// What to show for directories in the outline panel.
//
// 1. A folder icon:
// "icon"
// 2. A disclosure chevron:
// "chevron"
// 3. A disclosure chevron followed by a folder icon:
// "both"
"folder_indicator": "icon",
// Whether to show the git status in the outline panel.
"git_status": true,
// Amount of indentation for nested items.
@ -1014,10 +1028,17 @@
//
// Default: false
"file_icons": false,
// Whether to show folder icons or chevrons for directories in the git panel.
// What to show for directories in the git panel.
//
// Default: true
"folder_icons": true,
// 1. A folder icon:
// "icon"
// 2. A disclosure chevron:
// "chevron"
// 3. A disclosure chevron followed by a folder icon:
// "both"
//
// Default: icon
"folder_indicator": "icon",
// What branch name to use if `init.defaultBranch` is not set
//
// Default: main

View file

@ -14,5 +14,10 @@ doctest = false
[dependencies]
gpui.workspace = true
settings_content.workspace = true
theme.workspace = true
util.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }

View file

@ -2,6 +2,7 @@ use std::sync::Arc;
use std::{path::Path, str};
use gpui::{App, SharedString};
use settings_content::FolderIndicator;
use theme::{GlobalTheme, IconTheme, ThemeRegistry};
use util::paths::PathExt;
@ -10,6 +11,13 @@ pub struct FileIcons {
icon_theme: Arc<IconTheme>,
}
/// What a panel draws ahead of a directory's name, in render order.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct FolderIndicators {
pub chevron: Option<SharedString>,
pub icon: Option<SharedString>,
}
impl FileIcons {
pub fn get(cx: &App) -> Self {
Self {
@ -163,4 +171,116 @@ impl FileIcons {
.and_then(|icon_theme| get_chevron_icon(&icon_theme, expanded))
})
}
/// Resolves what a panel should draw ahead of a directory's name. Shared by every
/// panel that exposes a `folder_indicator` setting so they stay in agreement.
pub fn get_folder_indicators(
indicator: FolderIndicator,
expanded: bool,
path: &Path,
cx: &App,
) -> FolderIndicators {
let chevron = indicator
.shows_chevron()
.then(|| Self::get_chevron_icon(expanded, cx))
.flatten();
let icon = indicator
.shows_icon()
.then(|| Self::get_folder_icon(expanded, path, cx))
.flatten();
FolderIndicators { chevron, icon }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[gpui::test]
fn test_folder_indicators_per_setting(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);
});
let path = PathBuf::from("src");
cx.update(|cx| {
let icon_only =
FileIcons::get_folder_indicators(FolderIndicator::Icon, false, &path, cx);
assert_eq!(icon_only.chevron, None);
assert_eq!(
icon_only.icon,
Some("icons/file_icons/folder.svg".into()),
"`icon` should draw the folder icon and no chevron"
);
let chevron_only =
FileIcons::get_folder_indicators(FolderIndicator::Chevron, false, &path, cx);
assert_eq!(
chevron_only.chevron,
Some("icons/file_icons/chevron_right.svg".into())
);
assert_eq!(
chevron_only.icon, None,
"`chevron` should draw the chevron and no folder icon"
);
let both = FileIcons::get_folder_indicators(FolderIndicator::Both, false, &path, cx);
assert_eq!(
both.chevron,
Some("icons/file_icons/chevron_right.svg".into())
);
assert_eq!(both.icon, Some("icons/file_icons/folder.svg".into()));
});
}
#[gpui::test]
fn test_folder_indicators_reflect_expanded_state(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);
});
let path = PathBuf::from("src");
cx.update(|cx| {
let collapsed =
FileIcons::get_folder_indicators(FolderIndicator::Both, false, &path, cx);
assert_eq!(
collapsed.chevron,
Some("icons/file_icons/chevron_right.svg".into())
);
assert_eq!(collapsed.icon, Some("icons/file_icons/folder.svg".into()));
let expanded = FileIcons::get_folder_indicators(FolderIndicator::Both, true, &path, cx);
assert_eq!(
expanded.chevron,
Some("icons/file_icons/chevron_down.svg".into())
);
assert_eq!(
expanded.icon,
Some("icons/file_icons/folder_open.svg".into())
);
});
}
#[gpui::test]
fn test_folder_indicator_default_is_icon(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);
});
let path = PathBuf::from("src");
cx.update(|cx| {
let default =
FileIcons::get_folder_indicators(FolderIndicator::default(), false, &path, cx);
assert_eq!(
default,
FileIcons::get_folder_indicators(FolderIndicator::Icon, false, &path, cx),
"the default must stay `icon` so existing users see no change"
);
});
}
}

View file

@ -1228,7 +1228,7 @@ impl GitPanel {
let mut was_group_by = GitPanelSettings::get_global(cx).group_by;
let mut was_tree_view = GitPanelSettings::get_global(cx).tree_view;
let mut was_file_icons = GitPanelSettings::get_global(cx).file_icons;
let mut was_folder_icons = GitPanelSettings::get_global(cx).folder_icons;
let mut was_folder_indicator = GitPanelSettings::get_global(cx).folder_indicator;
let mut was_diff_stats = GitPanelSettings::get_global(cx).diff_stats;
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
let settings = GitPanelSettings::get_global(cx);
@ -1236,7 +1236,7 @@ impl GitPanel {
let group_by = settings.group_by;
let tree_view = settings.tree_view;
let file_icons = settings.file_icons;
let folder_icons = settings.folder_icons;
let folder_indicator = settings.folder_indicator;
let diff_stats = settings.diff_stats;
if tree_view != was_tree_view {
match (&mut this.view_mode, tree_view) {
@ -1263,14 +1263,14 @@ impl GitPanel {
if (diff_stats != was_diff_stats) || update_entries {
this.update_visible_entries(window, cx);
}
if file_icons != was_file_icons || folder_icons != was_folder_icons {
if file_icons != was_file_icons || folder_indicator != was_folder_indicator {
cx.notify();
}
was_sort_by = sort_by;
was_group_by = group_by;
was_tree_view = tree_view;
was_file_icons = file_icons;
was_folder_icons = folder_icons;
was_folder_indicator = folder_indicator;
was_diff_stats = diff_stats;
})
.detach();
@ -7874,24 +7874,35 @@ impl GitPanel {
)
};
let folder_indicator = settings.folder_indicator;
let name_row = h_flex()
.min_w_0()
.flex_1()
.gap_1()
.when(settings.file_icons, |this| {
this.child(
file_icon
.map(|file_icon| {
Icon::from_path(file_icon)
.size(IconSize::Small)
.color(Color::Muted)
})
.unwrap_or_else(|| {
Icon::new(IconName::File)
.size(IconSize::Small)
.color(Color::Muted)
}),
)
let icon = file_icon
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File))
.size(IconSize::Small)
.color(Color::Muted);
let reserves_chevron_slot =
tree_view && folder_indicator.shows_chevron() && folder_indicator.shows_icon();
if reserves_chevron_slot {
this.child(
h_flex()
.flex_none()
.gap_0p5()
.child(
h_flex()
.size(IconSize::Small.rems())
.invisible()
.flex_none(),
)
.child(icon),
)
} else {
this.child(icon)
}
})
.when(status_style != StatusStyle::LabelColor, |el| {
el.child(git_status_icon(status))
@ -8067,23 +8078,22 @@ impl GitPanel {
};
let settings = GitPanelSettings::get_global(cx);
let folder_icon = if settings.folder_icons {
FileIcons::get_folder_icon(entry.expanded, entry.key.path.as_std_path(), cx)
let folder_indicator = settings.folder_indicator;
let folder_indicators = FileIcons::get_folder_indicators(
folder_indicator,
entry.expanded,
entry.key.path.as_std_path(),
cx,
);
let fallback_chevron = if entry.expanded {
IconName::ChevronDown
} else {
FileIcons::get_chevron_icon(entry.expanded, cx)
IconName::ChevronRight
};
let fallback_folder_icon = if settings.folder_icons {
if entry.expanded {
IconName::FolderOpen
} else {
IconName::Folder
}
let fallback_folder_icon = if entry.expanded {
IconName::FolderOpen
} else {
if entry.expanded {
IconName::ChevronDown
} else {
IconName::ChevronRight
}
IconName::Folder
};
let stage_status = if let Some(repo) = &self.active_repository {
@ -8107,19 +8117,30 @@ impl GitPanel {
.min_w_0()
.gap_1()
.pl(px(entry.depth as f32 * TREE_INDENT))
.child(
folder_icon
.map(|folder_icon| {
Icon::from_path(folder_icon)
.size(IconSize::Small)
.color(Color::Muted)
})
.unwrap_or_else(|| {
Icon::new(fallback_folder_icon)
.size(IconSize::Small)
.color(Color::Muted)
}),
)
.child(h_flex().flex_none().gap_0p5().children({
let render_indicator = |themed: Option<SharedString>, fallback: IconName| {
themed
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(fallback))
.size(IconSize::Small)
.color(Color::Muted)
};
let mut indicators = Vec::new();
if folder_indicator.shows_chevron() {
indicators.push(render_indicator(
folder_indicators.chevron,
fallback_chevron,
));
}
if folder_indicator.shows_icon() {
indicators.push(render_indicator(
folder_indicators.icon,
fallback_folder_icon,
));
}
indicators
}))
.child(self.entry_label(entry.name.clone(), label_color).truncate());
h_flex()

View file

@ -3,8 +3,8 @@ use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{
GitPanelClickBehavior, GitPanelGroupBy, GitPanelSortBy, IntoGpui, RegisterSetting, Settings,
StatusStyle,
FolderIndicator, GitPanelClickBehavior, GitPanelGroupBy, GitPanelSortBy, IntoGpui,
RegisterSetting, Settings, StatusStyle,
};
use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
use workspace::dock::DockPosition;
@ -21,7 +21,7 @@ pub struct GitPanelSettings {
pub default_width: Pixels,
pub status_style: StatusStyle,
pub file_icons: bool,
pub folder_icons: bool,
pub folder_indicator: FolderIndicator,
pub scrollbar: ScrollbarSettings,
pub fallback_branch_name: String,
pub sort_by: GitPanelSortBy,
@ -64,7 +64,7 @@ impl Settings for GitPanelSettings {
default_width: git_panel.default_width.unwrap().into_gpui(),
status_style: git_panel.status_style.unwrap(),
file_icons: git_panel.file_icons.unwrap(),
folder_icons: git_panel.folder_icons.unwrap(),
folder_indicator: git_panel.folder_indicator.unwrap(),
scrollbar: ScrollbarSettings {
show: git_panel
.scrollbar

View file

@ -370,3 +370,9 @@ pub(crate) mod m_2026_08_17 {
pub(crate) use settings::make_git_gutter_width_an_enum;
}
pub(crate) mod m_2026_08_26 {
mod settings;
pub(crate) use settings::rename_folder_icons_to_folder_indicator;
}

View file

@ -0,0 +1,35 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
const PANEL_KEYS: &[&str] = &["project_panel", "outline_panel", "git_panel"];
const OLD_KEY: &str = "folder_icons";
const NEW_KEY: &str = "folder_indicator";
pub fn rename_folder_icons_to_folder_indicator(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(object: &mut serde_json::Map<String, Value>) -> Result<()> {
for panel_key in PANEL_KEYS {
let Some(panel) = object.get_mut(*panel_key).and_then(Value::as_object_mut) else {
continue;
};
// Anything other than a boolean never deserialized as `folder_icons`, so leave
// it in place rather than guessing which indicator was meant.
let indicator = match panel.get(OLD_KEY) {
Some(Value::Bool(true)) => "icon",
Some(Value::Bool(false)) => "chevron",
_ => continue,
};
panel.remove(OLD_KEY);
panel
.entry(NEW_KEY)
.or_insert_with(|| Value::String(indicator.to_string()));
}
Ok(())
}

View file

@ -258,6 +258,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
&SETTINGS_QUERY_2026_05_04,
),
MigrationType::Json(migrations::m_2026_08_17::make_git_gutter_width_an_enum),
MigrationType::Json(migrations::m_2026_08_26::rename_folder_icons_to_folder_indicator),
];
run_migrations(text, migrations)
}
@ -5481,4 +5482,211 @@ mod tests {
// no gutter key — no change
assert_migrate_settings(&r#"{ "theme": "One Dark" }"#.unindent(), None);
}
#[test]
fn test_rename_folder_icons_to_folder_indicator_in_all_panels() {
assert_migrate_with_migrations(
&[MigrationType::Json(
migrations::m_2026_08_26::rename_folder_icons_to_folder_indicator,
)],
&r#"
{
"project_panel": {
"folder_icons": true
},
"outline_panel": {
"folder_icons": false
},
"git_panel": {
"folder_icons": true
}
}
"#
.unindent(),
Some(
&r#"
{
"project_panel": {
"folder_indicator": "icon"
},
"outline_panel": {
"folder_indicator": "chevron"
},
"git_panel": {
"folder_indicator": "icon"
}
}
"#
.unindent(),
),
);
}
// The shared JSON migration driver applies a rename as a delete plus an add, and
// added keys are written to the front of their object. Comments and sibling values
// survive; only the renamed key's position moves.
#[test]
fn test_rename_folder_icons_to_folder_indicator_preserves_comments_and_siblings() {
assert_migrate_with_migrations(
&[MigrationType::Json(
migrations::m_2026_08_26::rename_folder_icons_to_folder_indicator,
)],
&r#"
{
// Keep this comment.
"project_panel": {
"file_icons": true,
"folder_icons": false,
"indent_size": 20
}
}
"#
.unindent(),
Some(
&r#"
{
// Keep this comment.
"project_panel": {
"folder_indicator": "chevron",
"file_icons": true,
"indent_size": 20
}
}
"#
.unindent(),
),
);
}
#[test]
fn test_rename_folder_icons_to_folder_indicator_in_platform_overrides() {
assert_migrate_with_migrations(
&[MigrationType::Json(
migrations::m_2026_08_26::rename_folder_icons_to_folder_indicator,
)],
&r#"
{
"macos": {
"project_panel": {
"folder_icons": false
}
}
}
"#
.unindent(),
Some(
&r#"
{
"macos": {
"project_panel": {
"folder_indicator": "chevron"
}
}
}
"#
.unindent(),
),
);
}
#[test]
fn test_rename_folder_icons_to_folder_indicator_does_not_clobber_new_key() {
assert_migrate_with_migrations(
&[MigrationType::Json(
migrations::m_2026_08_26::rename_folder_icons_to_folder_indicator,
)],
&r#"
{
"project_panel": {
"folder_icons": true,
"folder_indicator": "both"
}
}
"#
.unindent(),
Some(
&r#"
{
"project_panel": {
"folder_indicator": "both"
}
}
"#
.unindent(),
),
);
}
#[test]
fn test_rename_folder_icons_to_folder_indicator_no_change_cases() {
let migrations = &[MigrationType::Json(
migrations::m_2026_08_26::rename_folder_icons_to_folder_indicator,
)];
// Already migrated.
assert_migrate_with_migrations(
migrations,
&r#"
{
"project_panel": {
"folder_indicator": "both"
}
}
"#
.unindent(),
None,
);
// A non-boolean value is already invalid; leave it rather than guess.
assert_migrate_with_migrations(
migrations,
&r#"
{
"project_panel": {
"folder_icons": 3
}
}
"#
.unindent(),
None,
);
// `folder_icons` outside the three panels is not ours to rename.
assert_migrate_with_migrations(
migrations,
&r#"
{
"terminal": {
"folder_icons": true
}
}
"#
.unindent(),
None,
);
}
#[test]
fn test_rename_folder_icons_to_folder_indicator_is_registered() {
assert_migrate_settings(
&r#"
{
"project_panel": {
"folder_icons": false
}
}
"#
.unindent(),
Some(
&r#"
{
"project_panel": {
"folder_indicator": "chevron"
}
}
"#
.unindent(),
),
);
}
}

View file

@ -41,7 +41,7 @@ use std::{
u32,
};
use outline_panel_settings::{DockSide, OutlinePanelSettings, ShowIndentGuides};
use outline_panel_settings::{DockSide, FolderIndicator, OutlinePanelSettings, ShowIndentGuides};
use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem};
use search::{BufferSearchBar, ProjectSearchView};
use serde::{Deserialize, Serialize};
@ -2372,7 +2372,10 @@ impl OutlinePanel {
)
.color(color)
.into_any_element(),
icon.unwrap_or_else(empty_icon),
reserve_chevron_slot(
settings.folder_indicator,
icon.unwrap_or_else(empty_icon),
),
)
}
FsEntry::Directory(directory) => {
@ -2387,13 +2390,13 @@ impl OutlinePanel {
directory.entry.is_ignored,
is_active,
);
let icon = if settings.folder_icons {
FileIcons::get_folder_icon(is_expanded, directory.entry.path.as_std_path(), cx)
} else {
FileIcons::get_chevron_icon(is_expanded, cx)
}
.map(Icon::from_path)
.map(|icon| icon.color(color).into_any_element());
let icon = folder_indicator_element(
settings.folder_indicator,
is_expanded,
directory.entry.path.as_std_path(),
color,
cx,
);
(
ElementId::from(directory.entry.id.to_proto() as usize),
HighlightedLabel::new(
@ -2436,7 +2439,10 @@ impl OutlinePanel {
)
.color(color)
.into_any_element(),
icon.unwrap_or_else(empty_icon),
reserve_chevron_slot(
settings.folder_indicator,
icon.unwrap_or_else(empty_icon),
),
)
}
};
@ -2484,13 +2490,13 @@ impl OutlinePanel {
.map(|entry| entry.git_summary)
.unwrap_or_default();
let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
let icon = if settings.folder_icons {
FileIcons::get_folder_icon(is_expanded, &Path::new(&name), cx)
} else {
FileIcons::get_chevron_icon(is_expanded, cx)
}
.map(Icon::from_path)
.map(|icon| icon.color(color).into_any_element());
let icon = folder_indicator_element(
settings.folder_indicator,
is_expanded,
Path::new(&name),
color,
cx,
);
(
ElementId::from(
folded_dir
@ -2659,7 +2665,7 @@ impl OutlinePanel {
.toggle_state(is_active)
.child(
h_flex()
.child(h_flex().w(px(16.)).justify_center().child(icon_element))
.child(h_flex().min_w(px(16.)).justify_center().child(icon_element))
.child(h_flex().h_6().child(label_element).ml_1()),
)
.on_secondary_mouse_down(cx.listener(
@ -5317,6 +5323,48 @@ fn empty_icon() -> AnyElement {
.into_any_element()
}
fn folder_indicator_element(
indicator: FolderIndicator,
expanded: bool,
path: &Path,
color: Color,
cx: &App,
) -> Option<AnyElement> {
let indicators = FileIcons::get_folder_indicators(indicator, expanded, path, cx);
let render_indicator = |icon_path| Icon::from_path(icon_path).color(color);
match (indicators.chevron, indicators.icon) {
(Some(chevron), Some(icon)) => Some(
h_flex()
.flex_none()
.gap_0p5()
.child(render_indicator(chevron))
.child(render_indicator(icon))
.into_any_element(),
),
(Some(only), None) | (None, Some(only)) => Some(render_indicator(only).into_any_element()),
(None, None) => None,
}
}
/// Adds a blank as wide as a chevron in front of `icon` in `both` mode. Leaves `icon`
/// alone in the other modes.
///
/// Directories show a chevron and an icon in `both` mode. Without the blank, a file's
/// icon would line up under the folder chevrons instead of the folder icons.
fn reserve_chevron_slot(indicator: FolderIndicator, icon: AnyElement) -> AnyElement {
if indicator.shows_chevron() && indicator.shows_icon() {
h_flex()
.flex_none()
.gap_0p5()
.child(empty_icon())
.child(icon)
.into_any_element()
} else {
icon
}
}
#[derive(Debug, Default)]
struct GenerationState {
entries: Vec<CachedEntry>,

View file

@ -1,6 +1,6 @@
use editor::{EditorSettings, ui_scrollbar_settings_from_raw};
use gpui::{App, Pixels};
pub use settings::{DockSide, Settings, ShowIndentGuides};
pub use settings::{DockSide, FolderIndicator, Settings, ShowIndentGuides};
use settings::{IntoGpui, RegisterSetting};
use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
@ -10,7 +10,7 @@ pub struct OutlinePanelSettings {
pub default_width: Pixels,
pub dock: DockSide,
pub file_icons: bool,
pub folder_icons: bool,
pub folder_indicator: FolderIndicator,
pub git_status: bool,
pub indent_size: f32,
pub indent_guides: IndentGuidesSettings,
@ -53,7 +53,7 @@ impl Settings for OutlinePanelSettings {
default_width: panel.default_width.unwrap().into_gpui(),
dock: panel.dock.unwrap(),
file_icons: panel.file_icons.unwrap(),
folder_icons: panel.folder_icons.unwrap(),
folder_indicator: panel.folder_indicator.unwrap(),
git_status: panel.git_status.unwrap()
&& content
.git

View file

@ -269,6 +269,7 @@ impl DiagnosticCount {
#[derive(Debug, PartialEq, Eq, Clone)]
struct EntryDetails {
filename: String,
chevron: Option<SharedString>,
icon: Option<SharedString>,
path: Arc<RelPath>,
depth: usize,
@ -283,6 +284,8 @@ struct EntryDetails {
sticky: Option<StickyDetails>,
filename_text_color: Color,
diagnostic_severity: Option<DiagnosticSeverity>,
diagnostic_mark: Option<DiagnosticMark>,
reserves_chevron_slot: bool,
diagnostic_count: Option<DiagnosticCount>,
git_status: GitSummary,
is_private: bool,
@ -290,6 +293,18 @@ struct EntryDetails {
canonical_path: Option<Arc<Path>>,
}
/// The glyph a row's diagnostic mark decorates, and the shape of that mark.
///
/// A row has one indicator slot, so the mark decorates whichever glyph already fills
/// it rather than claiming a slot of its own. `Standalone` covers rows that draw no
/// glyph at all.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum DiagnosticMark {
OnIcon(IconDecorationKind),
OnChevron(IconDecorationKind),
Standalone(IconName),
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct StickyDetails {
sticky_index: usize,
@ -595,6 +610,7 @@ pub enum Event {
struct DraggedProjectEntryView {
selection: SelectedEntry,
chevron: Option<SharedString>,
icon: Option<SharedString>,
filename: String,
click_offset: Point<Pixels>,
@ -5751,6 +5767,7 @@ impl ProjectPanel {
let file_name = details.filename.clone();
let chevron = details.chevron.clone();
let mut icon = details.icon.clone();
if settings.file_icons && show_editor && details.kind.is_file() {
let filename = self.filename_editor.read(cx).text(cx);
@ -5761,6 +5778,8 @@ impl ProjectPanel {
let filename_text_color = details.filename_text_color;
let diagnostic_severity = details.diagnostic_severity;
let diagnostic_mark = details.diagnostic_mark;
let reserves_chevron_slot = details.reserves_chevron_slot;
let diagnostic_count = details.diagnostic_count;
let item_colors = get_item_color(is_sticky, cx);
@ -6057,6 +6076,7 @@ impl ProjectPanel {
.as_ref()
.unwrap_or_else(|| &details.filename);
cx.new(|_| DraggedProjectEntryView {
chevron: details.chevron.clone(),
icon: details.icon.clone(),
filename: filename.clone(),
click_offset,
@ -6280,55 +6300,88 @@ impl ProjectPanel {
)
},
)
.child(if let Some(icon) = &icon {
if let Some((_, decoration_color)) =
.map(|this| {
let decoration_color =
entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
{
let is_warning = diagnostic_severity
.map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
.unwrap_or(false);
div().child(
DecoratedIcon::new(
Icon::from_path(icon.clone()).color(Color::Muted),
Some(
IconDecoration::new(
if kind.is_file() {
if is_warning {
IconDecorationKind::Triangle
} else {
IconDecorationKind::X
}
} else {
IconDecorationKind::Dot
},
bg_color,
cx,
)
.map(|(_, color)| color);
let decorated = |glyph: SharedString,
decoration_kind: IconDecorationKind,
color: Color| {
DecoratedIcon::new(
Icon::from_path(glyph).color(Color::Muted),
Some(
IconDecoration::new(decoration_kind, bg_color, cx)
.group_name(Some(GROUP_NAME.into()))
.knockout_hover_color(bg_hover_color)
.color(decoration_color.color(cx))
.color(color.color(cx))
.position(Point {
x: px(-2.),
y: px(-2.),
}),
),
)
.into_any_element(),
),
)
.into_any_element()
};
let icon_slot = if let Some(icon) = &icon {
Some(match diagnostic_mark.zip(decoration_color) {
Some((DiagnosticMark::OnIcon(decoration_kind), color)) => {
div().child(decorated(icon.clone(), decoration_kind, color))
}
_ => h_flex()
.child(Icon::from_path(icon.to_string()).color(Color::Muted)),
})
} else if let Some(DiagnosticMark::Standalone(icon_name)) = diagnostic_mark
{
let color =
entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
.map_or(Color::Error, |(_, color)| color);
Some(
h_flex()
.size(IconSize::default().rems())
.child(Icon::new(icon_name).color(color).size(IconSize::Small)),
)
} else if chevron.is_some() {
// The chevron already fills this slot; a spacer would double its width.
None
} else {
h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
Some(
h_flex()
.size(IconSize::default().rems())
.invisible()
.flex_none(),
)
};
let chevron =
chevron.map(|chevron| match diagnostic_mark.zip(decoration_color) {
Some((DiagnosticMark::OnChevron(decoration_kind), color)) => {
decorated(chevron, decoration_kind, color)
}
_ => Icon::from_path(chevron)
.color(Color::Muted)
.into_any_element(),
});
match (chevron, icon_slot) {
(Some(chevron), Some(icon_slot)) => {
this.child(h_flex().gap_0p5().child(chevron).child(icon_slot))
}
(Some(chevron), None) => this.child(h_flex().child(chevron)),
(None, Some(icon_slot)) if reserves_chevron_slot => this.child(
h_flex()
.gap_0p5()
.child(
h_flex()
.size(IconSize::default().rems())
.invisible()
.flex_none(),
)
.child(icon_slot),
),
(None, Some(icon_slot)) => this.child(icon_slot),
(None, None) => this,
}
} else if let Some((icon_name, color)) =
entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
{
h_flex()
.size(IconSize::default().rems())
.child(Icon::new(icon_name).color(color).size(IconSize::Small))
} else {
h_flex()
.size(IconSize::default().rems())
.invisible()
.flex_none()
})
.child(if show_editor {
h_flex().h_6().w_full().child(self.filename_editor.clone())
@ -6624,9 +6677,9 @@ impl ProjectPanel {
_window: &mut Window,
cx: &mut Context<Self>,
) -> EntryDetails {
let (show_file_icons, show_folder_icons) = {
let (show_file_icons, folder_indicator) = {
let settings = ProjectPanelSettings::get_global(cx);
(settings.file_icons, settings.folder_icons)
(settings.file_icons, settings.folder_indicator)
};
let expanded_entry_ids = self
@ -6637,20 +6690,23 @@ impl ProjectPanel {
.unwrap_or(&[]);
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
let icon = match entry.kind {
let (chevron, icon) = match entry.kind {
EntryKind::File => {
if show_file_icons {
let icon = if show_file_icons {
FileIcons::get_icon(entry.path.as_std_path(), cx)
} else {
None
}
};
(None, icon)
}
_ => {
if show_folder_icons {
FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
} else {
FileIcons::get_chevron_icon(is_expanded, cx)
}
let indicator = FileIcons::get_folder_indicators(
folder_indicator,
is_expanded,
entry.path.as_std_path(),
cx,
);
(indicator.chevron, indicator.icon)
}
};
@ -6690,6 +6746,31 @@ impl ProjectPanel {
.get(&(worktree_id, entry.path.clone()))
.copied();
let diagnostic_mark = if icon.is_some() || chevron.is_some() {
entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity).map(
|(kind, _)| {
let kind = if entry.kind.is_file() {
kind
} else {
IconDecorationKind::Dot
};
if icon.is_some() {
DiagnosticMark::OnIcon(kind)
} else {
DiagnosticMark::OnChevron(kind)
}
},
)
} else {
entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
.map(|(name, _)| DiagnosticMark::Standalone(name))
};
// Only `both` makes a directory two glyphs wide, leaving file icons under the
// folder chevrons unless files hold that width open too.
let reserves_chevron_slot =
chevron.is_none() && folder_indicator.shows_chevron() && folder_indicator.shows_icon();
let filename_text_color =
entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
@ -6700,6 +6781,7 @@ impl ProjectPanel {
EntryDetails {
filename,
chevron,
icon,
path: entry.path.clone(),
depth,
@ -6714,6 +6796,8 @@ impl ProjectPanel {
sticky,
filename_text_color,
diagnostic_severity,
diagnostic_mark,
reserves_chevron_slot,
diagnostic_count,
git_status,
is_private: entry.is_private,
@ -7697,11 +7781,16 @@ impl Render for DraggedProjectEntryView {
if self.selections.len() > 1 && self.selections.contains(&self.selection) {
this.child(Label::new(format!("{} entries", self.selections.len())))
} else {
this.child(if let Some(icon) = &self.icon {
div().child(Icon::from_path(icon.clone()))
} else {
div()
})
this.child(
h_flex()
.gap_0p5()
.when_some(self.chevron.clone(), |this, chevron| {
this.child(Icon::from_path(chevron))
})
.when_some(self.icon.clone(), |this, icon| {
this.child(Icon::from_path(icon))
}),
)
.child(Label::new(self.filename.clone()))
}
}),

View file

@ -3,8 +3,8 @@ use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{
DockSide, IntoGpui, ProjectPanelEntrySpacing, ProjectPanelSortMode, ProjectPanelSortOrder,
RegisterSetting, Settings, ShowDiagnostics, ShowIndentGuides,
DockSide, FolderIndicator, IntoGpui, ProjectPanelEntrySpacing, ProjectPanelSortMode,
ProjectPanelSortOrder, RegisterSetting, Settings, ShowDiagnostics, ShowIndentGuides,
};
use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
@ -16,7 +16,7 @@ pub struct ProjectPanelSettings {
pub dock: DockSide,
pub entry_spacing: ProjectPanelEntrySpacing,
pub file_icons: bool,
pub folder_icons: bool,
pub folder_indicator: FolderIndicator,
pub git_status: bool,
pub indent_size: f32,
pub indent_guides: IndentGuidesSettings,
@ -101,7 +101,7 @@ impl Settings for ProjectPanelSettings {
dock: project_panel.dock.unwrap(),
entry_spacing: project_panel.entry_spacing.unwrap(),
file_icons: project_panel.file_icons.unwrap(),
folder_icons: project_panel.folder_icons.unwrap(),
folder_indicator: project_panel.folder_indicator.unwrap(),
git_status: project_panel.git_status.unwrap()
&& content
.git

View file

@ -7,11 +7,15 @@ use git::{
repository::{InitialGraphCommitData, LogSource, RepoPath},
};
use gpui::{Empty, Entity, TestAppContext, VisualTestContext};
use language::{
Diagnostic, DiagnosticEntry, DiagnosticMessage, DiagnosticSourceKind, LanguageServerId,
PointUtf16, Unclipped,
};
use menu::Cancel;
use pretty_assertions::assert_eq;
use project::{FakeFs, ProjectPath};
use serde_json::json;
use settings::{ProjectPanelAutoOpenSettings, SettingsStore, SplicingVec};
use settings::{FolderIndicator, ProjectPanelAutoOpenSettings, SettingsStore, SplicingVec};
use smallvec::smallvec;
use std::path::{Path, PathBuf};
use util::{path, paths::PathStyle, rel_path::rel_path};
@ -10538,6 +10542,90 @@ pub(crate) fn find_project_entry(
})
}
/// Each visible entry as `(filename, chevron, icon)`, with the icon-theme paths reduced
/// to their file names so assertions read clearly.
fn visible_entry_indicators(
panel: &Entity<ProjectPanel>,
range: Range<usize>,
cx: &mut VisualTestContext,
) -> Vec<(String, Option<String>, Option<String>)> {
let mut result = Vec::new();
panel.update_in(cx, |panel, window, cx| {
panel.for_each_visible_entry(range, window, cx, &mut |_, details, _, _| {
let indicator_name = |indicator: Option<SharedString>| {
indicator.map(|indicator| {
indicator
.rsplit('/')
.next()
.unwrap_or(indicator.as_ref())
.to_string()
})
};
result.push((
details.filename.clone(),
indicator_name(details.chevron.clone()),
indicator_name(details.icon),
));
});
});
result
}
fn set_folder_indicator(indicator: FolderIndicator, cx: &mut VisualTestContext) {
cx.update(|_, cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings
.project_panel
.get_or_insert_default()
.folder_indicator = Some(indicator);
});
});
});
}
fn visible_entry_chevron_slots(
panel: &Entity<ProjectPanel>,
range: Range<usize>,
cx: &mut VisualTestContext,
) -> Vec<(String, bool)> {
let mut result = Vec::new();
panel.update_in(cx, |panel, window, cx| {
panel.for_each_visible_entry(range, window, cx, &mut |_, details, _, _| {
result.push((details.filename.clone(), details.reserves_chevron_slot));
});
});
result
}
fn visible_entry_diagnostic_marks(
panel: &Entity<ProjectPanel>,
range: Range<usize>,
cx: &mut VisualTestContext,
) -> Vec<(String, Option<DiagnosticMark>)> {
let mut result = Vec::new();
panel.update_in(cx, |panel, window, cx| {
panel.for_each_visible_entry(range, window, cx, &mut |_, details, _, _| {
result.push((details.filename.clone(), details.diagnostic_mark));
});
});
result
}
fn mark_for(marks: &[(String, Option<DiagnosticMark>)], filename: &str) -> Option<DiagnosticMark> {
marks
.iter()
.find(|(name, _)| name == filename)
.and_then(|(_, mark)| *mark)
}
fn visible_entries_as_strings(
panel: &Entity<ProjectPanel>,
range: Range<usize>,
@ -11621,3 +11709,261 @@ async fn test_restore_file_prompt_escapes_markdown_in_file_name(cx: &mut gpui::T
assert_eq!(message, "Discard changes to `__init__.py`?");
}
#[gpui::test]
async fn test_folder_indicator_selection(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"dir": { "nested.rs": "" },
"file.rs": "",
}),
)
.await;
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panel = workspace.update_in(cx, ProjectPanel::new);
cx.run_until_parked();
set_folder_indicator(FolderIndicator::Icon, cx);
cx.run_until_parked();
let indicators = visible_entry_indicators(&panel, 0..50, cx);
assert_eq!(
indicators
.iter()
.map(|(name, chevron, _)| (name.as_str(), chevron.as_deref()))
.collect::<Vec<_>>(),
vec![("root", None), ("dir", None), ("file.rs", None)],
"`icon` must not produce a chevron on any row"
);
assert_eq!(
indicators
.iter()
.find(|(name, _, _)| name == "dir")
.and_then(|(_, _, icon)| icon.as_deref()),
Some("folder.svg"),
"a collapsed directory shows the collapsed folder icon"
);
set_folder_indicator(FolderIndicator::Chevron, cx);
cx.run_until_parked();
assert_eq!(
visible_entry_indicators(&panel, 0..50, cx)
.into_iter()
.find(|(name, _, _)| name == "dir"),
Some((
"dir".to_string(),
Some("chevron_right.svg".to_string()),
None
)),
"`chevron` swaps the folder icon out rather than adding to it"
);
set_folder_indicator(FolderIndicator::Both, cx);
cx.run_until_parked();
let indicators = visible_entry_indicators(&panel, 0..50, cx);
assert_eq!(
indicators.iter().find(|(name, _, _)| name == "dir"),
Some(&(
"dir".to_string(),
Some("chevron_right.svg".to_string()),
Some("folder.svg".to_string())
)),
"`both` shows the chevron and the folder icon together"
);
assert_eq!(
indicators
.iter()
.find(|(name, _, _)| name == "file.rs")
.map(|(_, chevron, _)| chevron.as_deref()),
Some(None),
"files never take a chevron, so they stay flush in `both`"
);
// Expanding must flip the chevron and the icon, not just the icon.
toggle_expand_dir(&panel, "root/dir", cx);
cx.run_until_parked();
assert_eq!(
visible_entry_indicators(&panel, 0..50, cx)
.into_iter()
.find(|(name, _, _)| name == "dir"),
Some((
"dir".to_string(),
Some("chevron_down.svg".to_string()),
Some("folder_open.svg".to_string())
)),
);
}
#[gpui::test]
async fn test_diagnostic_mark_decorates_the_row_glyph(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"dir": { "nested.rs": "" },
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panel = workspace.update_in(cx, ProjectPanel::new);
cx.run_until_parked();
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostic_entries(
LanguageServerId(0),
PathBuf::from(path!("/root/dir/nested.rs")),
None,
None,
vec![DiagnosticEntry::new(
Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 0)),
Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: DiagnosticMessage::from("an error"),
source_kind: DiagnosticSourceKind::Pushed,
is_primary: true,
..Default::default()
},
)],
cx,
)
.unwrap();
});
// The panel debounces diagnostic summary updates before rebuilding its entries.
cx.executor().advance_clock(Duration::from_millis(50));
cx.run_until_parked();
toggle_expand_dir(&panel, "root/dir", cx);
cx.run_until_parked();
for (indicator, expected_dir_mark, expected_file_mark) in [
(
FolderIndicator::Icon,
DiagnosticMark::OnIcon(IconDecorationKind::Dot),
DiagnosticMark::OnIcon(IconDecorationKind::X),
),
(
FolderIndicator::Chevron,
DiagnosticMark::OnChevron(IconDecorationKind::Dot),
DiagnosticMark::OnIcon(IconDecorationKind::X),
),
(
FolderIndicator::Both,
DiagnosticMark::OnIcon(IconDecorationKind::Dot),
DiagnosticMark::OnIcon(IconDecorationKind::X),
),
] {
set_folder_indicator(indicator, cx);
cx.run_until_parked();
let marks = visible_entry_diagnostic_marks(&panel, 0..50, cx);
assert_eq!(
mark_for(&marks, "dir"),
Some(expected_dir_mark),
"{indicator:?}: a directory carrying diagnostics marks the glyph its mode renders"
);
assert_eq!(
mark_for(&marks, "nested.rs"),
Some(expected_file_mark),
"{indicator:?}: a file always marks its own icon"
);
assert_eq!(
mark_for(&marks, "root"),
Some(expected_dir_mark),
"{indicator:?}: the worktree root rolls its children's diagnostics up"
);
}
// With no icon and no chevron there is no glyph to decorate, so the mark stands alone.
set_folder_indicator(FolderIndicator::Chevron, cx);
cx.update(|_, cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings.project_panel.get_or_insert_default().file_icons = Some(false);
});
});
});
cx.run_until_parked();
let marks = visible_entry_diagnostic_marks(&panel, 0..50, cx);
assert_eq!(
mark_for(&marks, "nested.rs"),
Some(DiagnosticMark::Standalone(IconName::Close)),
"a file with no icon has nothing to decorate"
);
assert_eq!(
mark_for(&marks, "dir"),
Some(DiagnosticMark::OnChevron(IconDecorationKind::Dot)),
"`file_icons` must not disturb how a directory is marked"
);
}
#[gpui::test]
async fn test_file_rows_reserve_the_chevron_slot(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"dir": { "nested.rs": "" },
"file.rs": "",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panel = workspace.update_in(cx, ProjectPanel::new);
cx.run_until_parked();
for (indicator, expected) in [
(FolderIndicator::Icon, false),
(FolderIndicator::Chevron, false),
(FolderIndicator::Both, true),
] {
set_folder_indicator(indicator, cx);
cx.run_until_parked();
let slots = visible_entry_chevron_slots(&panel, 0..50, cx);
assert_eq!(
slots
.iter()
.find(|(name, _)| name == "file.rs")
.map(|(_, reserved)| *reserved),
Some(expected),
"{indicator:?}: a file reserves the chevron's width only when directories draw one \
alongside their icon"
);
assert_eq!(
slots
.iter()
.find(|(name, _)| name == "dir")
.map(|(_, reserved)| *reserved),
Some(false),
"{indicator:?}: a directory draws its own chevron, so it reserves nothing"
);
}
}

View file

@ -680,7 +680,13 @@ impl VsCodeSettings {
fn outline_panel_settings_content(&self) -> Option<OutlinePanelSettingsContent> {
skip_default(OutlinePanelSettingsContent {
file_icons: self.read_bool("outline.icons"),
folder_icons: self.read_bool("outline.icons"),
folder_indicator: self.read_bool("outline.icons").map(|icons| {
if icons {
FolderIndicator::Icon
} else {
FolderIndicator::Chevron
}
}),
git_status: self.read_bool("git.decorations.enabled"),
..Default::default()
})
@ -823,7 +829,7 @@ impl VsCodeSettings {
drag_and_drop: None,
entry_spacing: None,
file_icons: None,
folder_icons: None,
folder_indicator: None,
git_status: self.read_bool("git.decorations.enabled"),
hide_gitignore: self.read_bool("explorer.excludeGitIgnore"),
hide_hidden: None,

View file

@ -747,10 +747,10 @@ pub struct GitPanelSettingsContent {
/// Default: false
pub file_icons: Option<bool>,
/// Whether to show folder icons or chevrons for directories in the git panel.
/// What to show for directories in the git panel.
///
/// Default: true
pub folder_icons: Option<bool>,
/// Default: icon
pub folder_indicator: Option<FolderIndicator>,
/// How and when the scrollbar should be displayed.
///
@ -1176,10 +1176,10 @@ pub struct OutlinePanelSettingsContent {
///
/// Default: true
pub file_icons: Option<bool>,
/// Whether to show folder icons or chevrons for directories in the outline panel.
/// What to show for directories in the outline panel.
///
/// Default: true
pub folder_icons: Option<bool>,
/// Default: icon
pub folder_indicator: Option<FolderIndicator>,
/// Whether to show the git status in the outline panel.
///
/// Default: true

View file

@ -778,10 +778,10 @@ pub struct ProjectPanelSettingsContent {
///
/// Default: true
pub file_icons: Option<bool>,
/// Whether to show folder icons or chevrons for directories in the project panel.
/// What to show for directories in the project panel.
///
/// Default: true
pub folder_icons: Option<bool>,
/// Default: icon
pub folder_indicator: Option<FolderIndicator>,
/// Whether to show the git status in the project panel.
///
/// Default: true
@ -878,6 +878,41 @@ pub enum ProjectPanelEntrySpacing {
Standard,
}
#[derive(
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum FolderIndicator {
/// Show a folder icon.
#[default]
Icon,
/// Show a disclosure chevron.
Chevron,
/// Show a disclosure chevron followed by a folder icon.
Both,
}
impl FolderIndicator {
pub fn shows_chevron(self) -> bool {
matches!(self, Self::Chevron | Self::Both)
}
pub fn shows_icon(self) -> bool {
matches!(self, Self::Icon | Self::Both)
}
}
#[derive(
Copy,
Clone,

View file

@ -5265,23 +5265,23 @@ fn panels_page() -> SettingsPage {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Folder Icons",
description: "Whether to show folder icons or chevrons for directories in the project panel.",
title: "Folder Indicator",
description: "What to show for directories in the project panel.",
field: Box::new(SettingField {
organization_override: None,
json_path: Some("project_panel.folder_icons"),
json_path: Some("project_panel.folder_indicator"),
pick: |settings_content| {
settings_content
.project_panel
.as_ref()?
.folder_icons
.folder_indicator
.as_ref()
},
write: |settings_content, value, _| {
settings_content
.project_panel
.get_or_insert_default()
.folder_icons = value;
.folder_indicator = value;
},
}),
metadata: None,
@ -5957,23 +5957,23 @@ fn panels_page() -> SettingsPage {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Folder Icons",
description: "Whether to show folder icons or chevrons for directories in the outline panel.",
title: "Folder Indicator",
description: "What to show for directories in the outline panel.",
field: Box::new(SettingField {
organization_override: None,
json_path: Some("outline_panel.folder_icons"),
json_path: Some("outline_panel.folder_indicator"),
pick: |settings_content| {
settings_content
.outline_panel
.as_ref()?
.folder_icons
.folder_indicator
.as_ref()
},
write: |settings_content, value, _| {
settings_content
.outline_panel
.get_or_insert_default()
.folder_icons = value;
.folder_indicator = value;
},
}),
metadata: None,
@ -6295,19 +6295,23 @@ fn panels_page() -> SettingsPage {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Folder Icons",
description: "Whether to show folder icons or chevrons for directories in the git panel.",
title: "Folder Indicator",
description: "What to show for directories in the git panel.",
field: Box::new(SettingField {
organization_override: None,
json_path: Some("git_panel.folder_icons"),
json_path: Some("git_panel.folder_indicator"),
pick: |settings_content| {
settings_content.git_panel.as_ref()?.folder_icons.as_ref()
settings_content
.git_panel
.as_ref()?
.folder_indicator
.as_ref()
},
write: |settings_content, value, _| {
settings_content
.git_panel
.get_or_insert_default()
.folder_icons = value;
.folder_indicator = value;
},
}),
metadata: None,

View file

@ -571,6 +571,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::ActivateOnClose>(render_dropdown)
.add_basic_renderer::<settings::ShowDiagnostics>(render_dropdown)
.add_basic_renderer::<settings::ShowCloseButton>(render_dropdown)
.add_basic_renderer::<settings::FolderIndicator>(render_dropdown)
.add_basic_renderer::<settings::ProjectPanelEntrySpacing>(render_dropdown)
.add_basic_renderer::<settings::ProjectPanelSortMode>(render_dropdown)
.add_basic_renderer::<settings::ProjectPanelSortOrder>(render_dropdown)

View file

@ -155,14 +155,14 @@ The following VS Code settings are automatically imported when you use **Import
**Other**
| VS Code Setting | Zed Setting |
| -------------------------- | -------------------------------------------------------- |
| `http.proxy` | `proxy` |
| `npm.packageManager` | `node.npm_path` |
| `telemetry.telemetryLevel` | `telemetry.metrics`, `telemetry.diagnostics` |
| `outline.icons` | `outline_panel.file_icons`, `outline_panel.folder_icons` |
| `chat.agent.enabled` | `agent.enabled` |
| `mcp` | `context_servers` |
| VS Code Setting | Zed Setting |
| -------------------------- | ------------------------------------------------------------ |
| `http.proxy` | `proxy` |
| `npm.packageManager` | `node.npm_path` |
| `telemetry.telemetryLevel` | `telemetry.metrics`, `telemetry.diagnostics` |
| `outline.icons` | `outline_panel.file_icons`, `outline_panel.folder_indicator` |
| `chat.agent.enabled` | `agent.enabled` |
| `mcp` | `context_servers` |
Zed doesn’t import extensions or keybindings, but this import gets core editor behavior close to your VS Code setup. If you skip that step during setup, you can still import settings manually later via the command palette:

View file

@ -5260,7 +5260,7 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
"dock": "right",
"entry_spacing": "comfortable",
"file_icons": true,
"folder_icons": true,
"folder_indicator": "icon",
"git_status": true,
"indent_size": 20,
"auto_reveal_entries": true,
@ -5797,7 +5797,7 @@ You can define these in user or project settings; project settings are merged on
"default_width": 300,
"dock": "left",
"file_icons": true,
"folder_icons": true,
"folder_indicator": "icon",
"git_status": true,
"indent_size": 20,
"auto_reveal_entries": true,

View file

@ -475,7 +475,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
"dock": "right", // Position of the dock (left, right)
"entry_spacing": "comfortable", // Vertical spacing (comfortable, standard)
"file_icons": true, // Show/hide file icons
"folder_icons": true, // Show/hide folder icons
"folder_indicator": "icon", // Dir glyph: icon, chevron, both
"git_status": true, // Indicate new/updated files
"indent_size": 20, // Pixels for each successive indent
"auto_reveal_entries": true, // Show file in panel when activating its buffer
@ -598,7 +598,7 @@ See [Terminal settings](./reference/all-settings.md#terminal) for additional non
"default_width": 300, // Default width of the git panel
"dock": "left", // Where to dock: left, right
"file_icons": true, // Show/hide file_icons
"folder_icons": true, // Show file_icons (true), chevrons (false) for dirs
"folder_indicator": "icon", // Dir glyph: icon, chevron, both
"git_status": true, // Show git status
"indent_size": 20, // Indentation for nested items (pixels)
"indent_guides": {