git: Fix a couple of bugs with buffer headers in the side-by-side diff UI (#47809)

- Fix sticky buffer header being painted on top of the toolbar
- Fix buffer header click handlers and tooltips sometimes not working
(element ID collision issue)
- Deduplicate buffer header rendering code between split and normal
editors

Release Notes:

- N/A
This commit is contained in:
Cole Miller authored and GitHub committed 2026-01-28 22:01:27 -05:00
1 parent a5d2dc5122
commit adf2cf9ba1
2 files changed
+462 -905

No files matched your search

+418 -415
View File
@@ -4092,421 +4092,16 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let editor = self.editor.read(cx);
let multi_buffer = editor.buffer.read(cx);
let is_read_only = self.editor.read(cx).read_only(cx);
let editor_handle: &dyn ItemHandle = &self.editor;
let breadcrumbs = if is_selected {
editor.breadcrumbs_inner(cx.theme(), cx)
} else {
None
};
let file_status = multi_buffer
.all_diff_hunks_expanded()
.then(|| editor.status_for_buffer_id(for_excerpt.buffer_id, cx))
.flatten();
let indicator = multi_buffer
.buffer(for_excerpt.buffer_id)
.and_then(|buffer| {
let buffer = buffer.read(cx);
let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) {
(true, _) => Some(Color::Warning),
(_, true) => Some(Color::Accent),
(false, false) => None,
};
indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color))
});
let include_root = editor
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let file = for_excerpt.buffer.file();
let can_open_excerpts = file.is_none_or(|file| file.can_open());
let path_style = file.map(|file| file.path_style(cx));
let relative_path = for_excerpt.buffer.resolve_file_path(include_root, cx);
let (parent_path, filename) = if let Some(path) = &relative_path {
if let Some(path_style) = path_style {
let (dir, file_name) = path_style.split(path);
(dir.map(|dir| dir.to_owned()), Some(file_name.to_owned()))
} else {
(None, Some(path.clone()))
}
} else {
(None, None)
};
let focus_handle = editor.focus_handle(cx);
let colors = cx.theme().colors();
let header = div()
.p_1()
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
.child(
h_flex()
.size_full()
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
.pl_1()
.pr_2()
.rounded_sm()
.gap_1p5()
.when(is_sticky, |el| el.shadow_md())
.border_1()
.map(|border| {
let border_color = if is_selected
&& is_folded
&& focus_handle.contains_focused(window, cx)
{
colors.border_focused
} else {
colors.border
};
border.border_color(border_color)
})
.bg(colors.editor_subheader_background)
.hover(|style| style.bg(colors.element_hover))
.map(|header| {
let editor = self.editor.clone();
let buffer_id = for_excerpt.buffer_id;
let toggle_chevron_icon =
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
let button_size = rems_from_px(28.);
header.child(
div()
.hover(|style| style.bg(colors.element_selected))
.rounded_xs()
.child(
ButtonLike::new("toggle-buffer-fold")
.style(ButtonStyle::Transparent)
.height(button_size.into())
.width(button_size)
.children(toggle_chevron_icon)
.tooltip({
let focus_handle = focus_handle.clone();
let is_folded_for_tooltip = is_folded;
move |_window, cx| {
Tooltip::with_meta_in(
if is_folded_for_tooltip {
"Unfold Excerpt"
} else {
"Fold Excerpt"
},
Some(&ToggleFold),
format!(
"{} to toggle all",
text_for_keystroke(
&Modifiers::alt(),
"click",
cx
)
),
&focus_handle,
cx,
)
}
})
.on_click(move |event, window, cx| {
if event.modifiers().alt {
// Alt+click toggles all buffers
editor.update(cx, |editor, cx| {
editor.toggle_fold_all(
&ToggleFoldAll,
window,
cx,
);
});
} else {
// Regular click toggles single buffer
if is_folded {
editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_id, cx);
});
} else {
editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_id, cx);
});
}
}
}),
),
)
})
.children(
editor
.addons
.values()
.filter_map(|addon| {
addon.render_buffer_header_controls(for_excerpt, window, cx)
})
.take(1),
)
.when(!is_read_only, |this| {
this.child(
h_flex()
.size_3()
.justify_center()
.flex_shrink_0()
.children(indicator),
)
})
.child(
h_flex()
.cursor_pointer()
.id("path_header_block")
.min_w_0()
.size_full()
.gap_1()
.justify_between()
.overflow_hidden()
.child(h_flex().min_w_0().flex_1().gap_0p5().overflow_hidden().map(
|path_header| {
let filename = filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into());
let full_path = match parent_path.as_deref() {
Some(parent) if !parent.is_empty() => {
format!("{}{}", parent, filename.as_str())
}
_ => filename.as_str().to_string(),
};
path_header
.child(
ButtonLike::new("filename-button")
.when(
ItemSettings::get_global(cx).file_icons,
|this| {
let path =
path::Path::new(filename.as_str());
let icon = FileIcons::get_icon(path, cx)
.unwrap_or_default();
this.child(
Icon::from_path(icon)
.color(Color::Muted),
)
},
)
.child(
Label::new(filename)
.single_line()
.color(file_status_label_color(file_status))
.buffer_font(cx)
.when(
file_status
.is_some_and(|s| s.is_deleted()),
|label| label.strikethrough(),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Open File",
None,
full_path.clone(),
cx,
)
})
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
)
.when_some(parent_path, |then, path| {
then.child(
Label::new(path)
.buffer_font(cx)
.truncate_start()
.color(
if file_status
.is_some_and(FileStatus::is_deleted)
{
Color::Custom(colors.text_disabled)
} else {
Color::Custom(colors.text_muted)
},
),
)
})
.when(!for_excerpt.buffer.capability.editable(), |el| {
el.child(
Icon::new(IconName::FileLock).color(Color::Muted),
)
})
.when_some(breadcrumbs, |then, breadcrumbs| {
then.child(render_breadcrumb_text(
breadcrumbs,
None,
editor_handle,
true,
window,
cx,
))
})
},
))
.when(
can_open_excerpts && is_selected && relative_path.is_some(),
|el| {
el.child(
Button::new("open-file-button", "Open File")
.style(ButtonStyle::OutlinedGhost)
.key_binding(KeyBinding::for_action_in(
&OpenExcerpts,
&focus_handle,
cx,
))
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
)
},
)
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(window.listener_for(&self.editor, {
let buffer_id = for_excerpt.buffer_id;
move |editor, e: &ClickEvent, window, cx| {
if e.modifiers().alt {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
return;
}
if is_folded {
editor.unfold_buffer(buffer_id, cx);
} else {
editor.fold_buffer(buffer_id, cx);
}
}
})),
),
);
let file = for_excerpt.buffer.file().cloned();
let editor = self.editor.clone();
right_click_menu("buffer-header-context-menu")
.trigger(move |_, _, _| header)
.menu(move |window, cx| {
let menu_context = focus_handle.clone();
let editor = editor.clone();
let file = file.clone();
ContextMenu::build(window, cx, move |mut menu, window, cx| {
if let Some(file) = file
&& let Some(project) = editor.read(cx).project()
&& let Some(worktree) =
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
{
let path_style = file.path_style(cx);
let worktree = worktree.read(cx);
let relative_path = file.path();
let entry_for_path = worktree.entry_for_path(relative_path);
let abs_path = entry_for_path.map(|e| {
e.canonical_path.as_deref().map_or_else(
|| worktree.absolutize(relative_path),
Path::to_path_buf,
)
});
let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
let parent_abs_path = abs_path
.as_ref()
.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
let relative_path = has_relative_path
.then_some(relative_path)
.map(ToOwned::to_owned);
let visible_in_project_panel =
relative_path.is_some() && worktree.is_visible();
let reveal_in_project_panel = entry_for_path
.filter(|_| visible_in_project_panel)
.map(|entry| entry.id);
menu = menu
.when_some(abs_path, |menu, abs_path| {
menu.entry(
"Copy Path",
Some(Box::new(zed_actions::workspace::CopyPath)),
window.handler_for(&editor, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
abs_path.to_string_lossy().into_owned(),
));
}),
)
})
.when_some(relative_path, |menu, relative_path| {
menu.entry(
"Copy Relative Path",
Some(Box::new(zed_actions::workspace::CopyRelativePath)),
window.handler_for(&editor, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
relative_path.display(path_style).to_string(),
));
}),
)
})
.when(
reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
|menu| menu.separator(),
)
.when_some(reveal_in_project_panel, |menu, entry_id| {
menu.entry(
"Reveal In Project Panel",
Some(Box::new(RevealInProjectPanel::default())),
window.handler_for(&editor, move |editor, _, cx| {
if let Some(project) = &mut editor.project {
project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(
entry_id,
))
});
}
}),
)
})
.when_some(parent_abs_path, |menu, parent_abs_path| {
menu.entry(
"Open in Terminal",
Some(Box::new(OpenInTerminal)),
window.handler_for(&editor, move |_, window, cx| {
window.dispatch_action(
OpenTerminal {
working_directory: parent_abs_path.clone(),
local: false,
}
.boxed_clone(),
cx,
);
}),
)
});
}
menu.context(menu_context)
})
})
render_buffer_header(
&self.editor,
for_excerpt,
is_folded,
is_selected,
is_sticky,
jump_data,
window,
cx,
)
}
fn render_blocks(
@@ -8441,6 +8036,414 @@ fn header_jump_data_inner(
}
}
pub(crate) fn render_buffer_header(
editor: &Entity<Editor>,
for_excerpt: &ExcerptInfo,
is_folded: bool,
is_selected: bool,
is_sticky: bool,
jump_data: JumpData,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let editor_read = editor.read(cx);
let multi_buffer = editor_read.buffer.read(cx);
let is_read_only = editor_read.read_only(cx);
let editor_handle: &dyn ItemHandle = editor;
let breadcrumbs = if is_selected {
editor_read.breadcrumbs_inner(cx.theme(), cx)
} else {
None
};
let file_status = multi_buffer
.all_diff_hunks_expanded()
.then(|| editor_read.status_for_buffer_id(for_excerpt.buffer_id, cx))
.flatten();
let indicator = multi_buffer
.buffer(for_excerpt.buffer_id)
.and_then(|buffer| {
let buffer = buffer.read(cx);
let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) {
(true, _) => Some(Color::Warning),
(_, true) => Some(Color::Accent),
(false, false) => None,
};
indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color))
});
let include_root = editor_read
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let file = for_excerpt.buffer.file();
let can_open_excerpts = file.is_none_or(|file| file.can_open());
let path_style = file.map(|file| file.path_style(cx));
let relative_path = for_excerpt.buffer.resolve_file_path(include_root, cx);
let (parent_path, filename) = if let Some(path) = &relative_path {
if let Some(path_style) = path_style {
let (dir, file_name) = path_style.split(path);
(dir.map(|dir| dir.to_owned()), Some(file_name.to_owned()))
} else {
(None, Some(path.clone()))
}
} else {
(None, None)
};
let focus_handle = editor_read.focus_handle(cx);
let colors = cx.theme().colors();
let header = div()
.id(("buffer-header", for_excerpt.buffer_id.to_proto()))
.p_1()
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
.child(
h_flex()
.size_full()
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
.pl_1()
.pr_2()
.rounded_sm()
.gap_1p5()
.when(is_sticky, |el| el.shadow_md())
.border_1()
.map(|border| {
let border_color =
if is_selected && is_folded && focus_handle.contains_focused(window, cx) {
colors.border_focused
} else {
colors.border
};
border.border_color(border_color)
})
.bg(colors.editor_subheader_background)
.hover(|style| style.bg(colors.element_hover))
.map(|header| {
let editor = editor.clone();
let buffer_id = for_excerpt.buffer_id;
let toggle_chevron_icon =
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
let button_size = rems_from_px(28.);
header.child(
div()
.hover(|style| style.bg(colors.element_selected))
.rounded_xs()
.child(
ButtonLike::new("toggle-buffer-fold")
.style(ButtonStyle::Transparent)
.height(button_size.into())
.width(button_size)
.children(toggle_chevron_icon)
.tooltip({
let focus_handle = focus_handle.clone();
let is_folded_for_tooltip = is_folded;
move |_window, cx| {
Tooltip::with_meta_in(
if is_folded_for_tooltip {
"Unfold Excerpt"
} else {
"Fold Excerpt"
},
Some(&ToggleFold),
format!(
"{} to toggle all",
text_for_keystroke(
&Modifiers::alt(),
"click",
cx
)
),
&focus_handle,
cx,
)
}
})
.on_click(move |event, window, cx| {
if event.modifiers().alt {
editor.update(cx, |editor, cx| {
editor.toggle_fold_all(&ToggleFoldAll, window, cx);
});
} else {
if is_folded {
editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_id, cx);
});
} else {
editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_id, cx);
});
}
}
}),
),
)
})
.children(
editor_read
.addons
.values()
.filter_map(|addon| {
addon.render_buffer_header_controls(for_excerpt, window, cx)
})
.take(1),
)
.when(!is_read_only, |this| {
this.child(
h_flex()
.size_3()
.justify_center()
.flex_shrink_0()
.children(indicator),
)
})
.child(
h_flex()
.cursor_pointer()
.id("path_header_block")
.min_w_0()
.size_full()
.gap_1()
.justify_between()
.overflow_hidden()
.child(h_flex().min_w_0().flex_1().gap_0p5().overflow_hidden().map(
|path_header| {
let filename = filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into());
let full_path = match parent_path.as_deref() {
Some(parent) if !parent.is_empty() => {
format!("{}{}", parent, filename.as_str())
}
_ => filename.as_str().to_string(),
};
path_header
.child(
ButtonLike::new("filename-button")
.when(ItemSettings::get_global(cx).file_icons, |this| {
let path = path::Path::new(filename.as_str());
let icon = FileIcons::get_icon(path, cx)
.unwrap_or_default();
this.child(
Icon::from_path(icon).color(Color::Muted),
)
})
.child(
Label::new(filename)
.single_line()
.color(file_status_label_color(file_status))
.buffer_font(cx)
.when(
file_status.is_some_and(|s| s.is_deleted()),
|label| label.strikethrough(),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Open File",
None,
full_path.clone(),
cx,
)
})
.on_click(window.listener_for(editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
)
.when_some(parent_path, |then, path| {
then.child(
Label::new(path)
.buffer_font(cx)
.truncate_start()
.color(
if file_status
.is_some_and(FileStatus::is_deleted)
{
Color::Custom(colors.text_disabled)
} else {
Color::Custom(colors.text_muted)
},
),
)
})
.when(!for_excerpt.buffer.capability.editable(), |el| {
el.child(Icon::new(IconName::FileLock).color(Color::Muted))
})
.when_some(breadcrumbs, |then, breadcrumbs| {
then.child(render_breadcrumb_text(
breadcrumbs,
None,
editor_handle,
true,
window,
cx,
))
})
},
))
.when(
can_open_excerpts && is_selected && relative_path.is_some(),
|el| {
el.child(
Button::new("open-file-button", "Open File")
.style(ButtonStyle::OutlinedGhost)
.key_binding(KeyBinding::for_action_in(
&OpenExcerpts,
&focus_handle,
cx,
))
.on_click(window.listener_for(editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
)
},
)
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(window.listener_for(editor, {
let buffer_id = for_excerpt.buffer_id;
move |editor, e: &ClickEvent, window, cx| {
if e.modifiers().alt {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
return;
}
if is_folded {
editor.unfold_buffer(buffer_id, cx);
} else {
editor.fold_buffer(buffer_id, cx);
}
}
})),
),
);
let file = for_excerpt.buffer.file().cloned();
let editor = editor.clone();
right_click_menu("buffer-header-context-menu")
.trigger(move |_, _, _| header)
.menu(move |window, cx| {
let menu_context = focus_handle.clone();
let editor = editor.clone();
let file = file.clone();
ContextMenu::build(window, cx, move |mut menu, window, cx| {
if let Some(file) = file
&& let Some(project) = editor.read(cx).project()
&& let Some(worktree) =
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
{
let path_style = file.path_style(cx);
let worktree = worktree.read(cx);
let relative_path = file.path();
let entry_for_path = worktree.entry_for_path(relative_path);
let abs_path = entry_for_path.map(|e| {
e.canonical_path
.as_deref()
.map_or_else(|| worktree.absolutize(relative_path), Path::to_path_buf)
});
let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
let parent_abs_path = abs_path
.as_ref()
.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
let relative_path = has_relative_path
.then_some(relative_path)
.map(ToOwned::to_owned);
let visible_in_project_panel = relative_path.is_some() && worktree.is_visible();
let reveal_in_project_panel = entry_for_path
.filter(|_| visible_in_project_panel)
.map(|entry| entry.id);
menu = menu
.when_some(abs_path, |menu, abs_path| {
menu.entry(
"Copy Path",
Some(Box::new(zed_actions::workspace::CopyPath)),
window.handler_for(&editor, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
abs_path.to_string_lossy().into_owned(),
));
}),
)
})
.when_some(relative_path, |menu, relative_path| {
menu.entry(
"Copy Relative Path",
Some(Box::new(zed_actions::workspace::CopyRelativePath)),
window.handler_for(&editor, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
relative_path.display(path_style).to_string(),
));
}),
)
})
.when(
reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
|menu| menu.separator(),
)
.when_some(reveal_in_project_panel, |menu, entry_id| {
menu.entry(
"Reveal In Project Panel",
Some(Box::new(RevealInProjectPanel::default())),
window.handler_for(&editor, move |editor, _, cx| {
if let Some(project) = &mut editor.project {
project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry_id))
});
}
}),
)
})
.when_some(parent_abs_path, |menu, parent_abs_path| {
menu.entry(
"Open in Terminal",
Some(Box::new(OpenInTerminal)),
window.handler_for(&editor, move |_, window, cx| {
window.dispatch_action(
OpenTerminal {
working_directory: parent_abs_path.clone(),
local: false,
}
.boxed_clone(),
cx,
);
}),
)
});
}
menu.context(menu_context)
})
})
}
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
impl AcceptEditPredictionBinding {
+44 -490
View File
@@ -1,33 +1,26 @@
use std::{cmp, collections::HashMap, path, path::Path};
use std::cmp;
use collections::HashSet;
use file_icons::FileIcons;
use git::status::FileStatus;
use collections::{HashMap, HashSet};
use gpui::{
AbsoluteLength, Action, AnyElement, App, AvailableSpace, Bounds, ClickEvent, ClipboardItem,
Context, DragMoveEvent, Element, Entity, Focusable, GlobalElementId, Hsla, InspectorElementId,
IntoElement, LayoutId, Length, Modifiers, MouseButton, ParentElement, Pixels,
StatefulInteractiveElement, Styled, TextStyleRefinement, Window, div, linear_color_stop,
linear_gradient, point, px, size,
AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, Context, DragMoveEvent, Element,
Entity, GlobalElementId, Hsla, InspectorElementId, IntoElement, LayoutId, Length,
ParentElement, Pixels, StatefulInteractiveElement, Styled, TextStyleRefinement, Window, div,
linear_color_stop, linear_gradient, point, px, size,
};
use multi_buffer::{Anchor, ExcerptId, ExcerptInfo};
use project::Entry;
use multi_buffer::{Anchor, ExcerptId};
use settings::Settings;
use text::BufferId;
use theme::ActiveTheme;
use ui::scrollbars::ShowScrollbar;
use ui::{
Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName, Indicator, KeyBinding, Label,
Tooltip, h_flex, prelude::*, right_click_menu, text_for_keystroke, v_flex,
};
use workspace::{ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel};
use ui::{h_flex, prelude::*, v_flex};
use gpui::ContentMask;
use crate::{
DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, JumpData,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, RowExt, StickyHeaderExcerpt, ToggleFold,
ToggleFoldAll,
DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, RowExt, StickyHeaderExcerpt,
display_map::Block,
element::{EditorElement, SplitSide},
element::{EditorElement, SplitSide, header_jump_data, render_buffer_header},
scroll::ScrollOffset,
split::SplittableEditor,
};
@@ -319,7 +312,7 @@ impl Element for SplitBufferHeadersElement {
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
@@ -334,13 +327,15 @@ impl Element for SplitBufferHeadersElement {
window.with_rem_size(rem_size, |window| {
window.with_text_style(Some(text_style), |window| {
for header_layout in &mut prepaint.non_sticky_headers {
header_layout.element.paint(window, cx);
}
window.with_content_mask(Some(ContentMask { bounds }), |window| {
for header_layout in &mut prepaint.non_sticky_headers {
header_layout.element.paint(window, cx);
}
if let Some(mut sticky_header) = prepaint.sticky_header.take() {
sticky_header.paint(window, cx);
}
if let Some(mut sticky_header) = prepaint.sticky_header.take() {
sticky_header.paint(window, cx);
}
});
});
});
}
@@ -532,8 +527,17 @@ impl SplitBufferHeadersElement {
.top_0(),
)
.child(
self.render_buffer_header(excerpt, false, selected, true, jump_data, window, cx)
.into_any_element(),
render_buffer_header(
&self.editor,
excerpt,
false,
selected,
true,
jump_data,
window,
cx,
)
.into_any_element(),
)
.into_any_element();
@@ -606,9 +610,17 @@ impl SplitBufferHeadersElement {
latest_selection_anchors,
);
let mut header = self
.render_buffer_header(excerpt, is_folded, selected, false, jump_data, window, cx)
.into_any_element();
let mut header = render_buffer_header(
&self.editor,
excerpt,
is_folded,
selected,
false,
jump_data,
window,
cx,
)
.into_any_element();
let y_offset = (block_row.0 as f64 - scroll_position.y) * f64::from(line_height);
let origin = point(bounds.origin.x, bounds.origin.y + Pixels::from(y_offset));
@@ -625,462 +637,4 @@ impl SplitBufferHeadersElement {
headers
}
fn render_buffer_header(
&self,
for_excerpt: &ExcerptInfo,
is_folded: bool,
is_selected: bool,
is_sticky: bool,
jump_data: JumpData,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let editor = self.editor.read(cx);
let multi_buffer = editor.buffer.read(cx);
let is_read_only = self.editor.read(cx).read_only(cx);
let file_status = multi_buffer
.all_diff_hunks_expanded()
.then(|| editor.status_for_buffer_id(for_excerpt.buffer_id, cx))
.flatten();
let indicator = multi_buffer
.buffer(for_excerpt.buffer_id)
.and_then(|buffer| {
let buffer = buffer.read(cx);
let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) {
(true, _) => Some(Color::Warning),
(_, true) => Some(Color::Accent),
(false, false) => None,
};
indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color))
});
let include_root = editor
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let file = for_excerpt.buffer.file();
let can_open_excerpts = file.is_none_or(|file| file.can_open());
let path_style = file.map(|file| file.path_style(cx));
let relative_path = for_excerpt.buffer.resolve_file_path(include_root, cx);
let (parent_path, filename) = if let Some(path) = &relative_path {
if let Some(path_style) = path_style {
let (dir, file_name) = path_style.split(path);
(dir.map(|dir| dir.to_owned()), Some(file_name.to_owned()))
} else {
(None, Some(path.clone()))
}
} else {
(None, None)
};
let focus_handle = self.editor.read(cx).focus_handle(cx);
let colors = cx.theme().colors();
let header = div()
.p_1()
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
.child(
h_flex()
.size_full()
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
.pl_1()
.pr_2()
.rounded_sm()
.gap_1p5()
.when(is_sticky, |el| el.shadow_md())
.border_1()
.map(|border| {
let border_color = if !is_sticky
&& is_selected
&& is_folded
&& focus_handle.contains_focused(window, cx)
{
colors.border_focused
} else {
colors.border
};
border.border_color(border_color)
})
.bg(colors.editor_subheader_background)
.hover(|style| style.bg(colors.element_hover))
.map(|header| {
let editor = self.editor.clone();
let buffer_id = for_excerpt.buffer_id;
let toggle_chevron_icon =
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
let button_size = rems_from_px(28.);
header.child(
div()
.hover(|style| style.bg(colors.element_selected))
.rounded_xs()
.child(
ButtonLike::new("toggle-buffer-fold")
.style(ButtonStyle::Transparent)
.height(button_size.into())
.width(button_size)
.children(toggle_chevron_icon)
.tooltip({
let focus_handle = focus_handle.clone();
let is_folded_for_tooltip = is_folded;
move |_window, cx| {
Tooltip::with_meta_in(
if is_folded_for_tooltip {
"Unfold Excerpt"
} else {
"Fold Excerpt"
},
Some(&ToggleFold),
format!(
"{} to toggle all",
text_for_keystroke(
&Modifiers::alt(),
"click",
cx
)
),
&focus_handle,
cx,
)
}
})
.on_click(move |event, window, cx| {
if event.modifiers().alt {
editor.update(cx, |editor, cx| {
editor.toggle_fold_all(
&ToggleFoldAll,
window,
cx,
);
});
} else {
if is_folded {
editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_id, cx);
});
} else {
editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_id, cx);
});
}
}
}),
),
)
})
.children(
editor
.addons
.values()
.filter_map(|addon| {
addon.render_buffer_header_controls(for_excerpt, window, cx)
})
.take(1),
)
.when(!is_read_only, |this| {
this.child(
h_flex()
.size_3()
.justify_center()
.flex_shrink_0()
.children(indicator),
)
})
.child(
h_flex()
.cursor_pointer()
.id("path_header_block")
.min_w_0()
.size_full()
.justify_between()
.overflow_hidden()
.child(h_flex().min_w_0().flex_1().gap_0p5().map(|path_header| {
let filename = filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into());
path_header
.when(ItemSettings::get_global(cx).file_icons, |el| {
let path = path::Path::new(filename.as_str());
let icon =
FileIcons::get_icon(path, cx).unwrap_or_default();
el.child(Icon::from_path(icon).color(Color::Muted))
})
.child(
ButtonLike::new("filename-button")
.child(
Label::new(filename)
.single_line()
.color(file_status_label_color(file_status))
.when(
file_status.is_some_and(|s| s.is_deleted()),
|label| label.strikethrough(),
),
)
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
)
.when(!for_excerpt.buffer.capability.editable(), |el| {
el.child(Icon::new(IconName::FileLock).color(Color::Muted))
})
.when_some(parent_path, |then, path| {
then.child(Label::new(path).truncate().color(
if file_status.is_some_and(FileStatus::is_deleted) {
Color::Custom(colors.text_disabled)
} else {
Color::Custom(colors.text_muted)
},
))
})
}))
.when(
can_open_excerpts && is_selected && relative_path.is_some(),
|el| {
el.child(
Button::new("open-file-button", "Open File")
.style(ButtonStyle::OutlinedGhost)
.key_binding(KeyBinding::for_action_in(
&OpenExcerpts,
&focus_handle,
cx,
))
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
)
},
)
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(window.listener_for(&self.editor, {
let buffer_id = for_excerpt.buffer_id;
move |editor, e: &ClickEvent, window, cx| {
if e.modifiers().alt {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
return;
}
if is_folded {
editor.unfold_buffer(buffer_id, cx);
} else {
editor.fold_buffer(buffer_id, cx);
}
}
})),
),
);
let file = for_excerpt.buffer.file().cloned();
let editor = self.editor.clone();
right_click_menu("buffer-header-context-menu")
.trigger(move |_, _, _| header)
.menu(move |window, cx| {
let menu_context = focus_handle.clone();
let editor = editor.clone();
let file = file.clone();
ContextMenu::build(window, cx, move |mut menu, window, cx| {
if let Some(file) = file
&& let Some(project) = editor.read(cx).project()
&& let Some(worktree) =
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
{
let path_style = file.path_style(cx);
let worktree = worktree.read(cx);
let relative_path = file.path();
let entry_for_path = worktree.entry_for_path(relative_path);
let abs_path = entry_for_path.map(|e| {
e.canonical_path.as_deref().map_or_else(
|| worktree.absolutize(relative_path),
Path::to_path_buf,
)
});
let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
let parent_abs_path = abs_path
.as_ref()
.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
let relative_path = has_relative_path
.then_some(relative_path)
.map(ToOwned::to_owned);
let visible_in_project_panel =
relative_path.is_some() && worktree.is_visible();
let reveal_in_project_panel = entry_for_path
.filter(|_| visible_in_project_panel)
.map(|entry| entry.id);
menu = menu
.when_some(abs_path, |menu, abs_path| {
menu.entry(
"Copy Path",
Some(Box::new(zed_actions::workspace::CopyPath)),
window.handler_for(&editor, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
abs_path.to_string_lossy().into_owned(),
));
}),
)
})
.when_some(relative_path, |menu, relative_path| {
menu.entry(
"Copy Relative Path",
Some(Box::new(zed_actions::workspace::CopyRelativePath)),
window.handler_for(&editor, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
relative_path.display(path_style).to_string(),
));
}),
)
})
.when(
reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
|menu| menu.separator(),
)
.when_some(reveal_in_project_panel, |menu, entry_id| {
menu.entry(
"Reveal In Project Panel",
Some(Box::new(RevealInProjectPanel::default())),
window.handler_for(&editor, move |editor, _, cx| {
if let Some(project) = &mut editor.project {
project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(
entry_id,
))
});
}
}),
)
})
.when_some(parent_abs_path, |menu, parent_abs_path| {
menu.entry(
"Open in Terminal",
Some(Box::new(OpenInTerminal)),
window.handler_for(&editor, move |_, window, cx| {
window.dispatch_action(
OpenTerminal {
working_directory: parent_abs_path.clone(),
local: false,
}
.boxed_clone(),
cx,
);
}),
)
});
}
menu.context(menu_context)
})
})
}
}
fn header_jump_data(
editor_snapshot: &EditorSnapshot,
block_row_start: DisplayRow,
height: u32,
first_excerpt: &ExcerptInfo,
latest_selection_anchors: &HashMap<BufferId, Anchor>,
) -> JumpData {
let jump_target = if let Some(anchor) = latest_selection_anchors.get(&first_excerpt.buffer_id)
&& let Some(range) = editor_snapshot.context_range_for_excerpt(anchor.excerpt_id)
&& let Some(buffer) = editor_snapshot
.buffer_snapshot()
.buffer_for_excerpt(anchor.excerpt_id)
{
JumpTargetInExcerptInput {
id: anchor.excerpt_id,
buffer,
excerpt_start_anchor: range.start,
jump_anchor: anchor.text_anchor,
}
} else {
JumpTargetInExcerptInput {
id: first_excerpt.id,
buffer: &first_excerpt.buffer,
excerpt_start_anchor: first_excerpt.range.context.start,
jump_anchor: first_excerpt.range.primary.start,
}
};
header_jump_data_inner(editor_snapshot, block_row_start, height, &jump_target)
}
struct JumpTargetInExcerptInput<'a> {
id: ExcerptId,
buffer: &'a language::BufferSnapshot,
excerpt_start_anchor: text::Anchor,
jump_anchor: text::Anchor,
}
fn header_jump_data_inner(
snapshot: &EditorSnapshot,
block_row_start: DisplayRow,
height: u32,
for_excerpt: &JumpTargetInExcerptInput,
) -> JumpData {
let buffer = &for_excerpt.buffer;
let jump_position = language::ToPoint::to_point(&for_excerpt.jump_anchor, buffer);
let excerpt_start = for_excerpt.excerpt_start_anchor;
let rows_from_excerpt_start = if for_excerpt.jump_anchor == excerpt_start {
0
} else {
let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
jump_position.row.saturating_sub(excerpt_start_point.row)
};
let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start)
.saturating_sub(
snapshot
.scroll_anchor
.scroll_position(&snapshot.display_snapshot)
.y as u32,
);
JumpData::MultiBufferPoint {
excerpt_id: for_excerpt.id,
anchor: for_excerpt.jump_anchor,
position: jump_position,
line_offset_from_top,
}
}
fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
file_status.map_or(Color::Default, |status| {
if status.is_conflicted() {
Color::Conflict
} else if status.is_modified() {
Color::Modified
} else if status.is_deleted() {
Color::Disabled
} else if status.is_created() {
Color::Created
} else {
Color::Default
}
})
}