zed/crates/editor/src/element/header.rs
Mikhail Pertsev 85f8bf7393
editor: Extract header and mouse out of element.rs (#57472)
cc @SomeoneToIgnore

## Summary

Follow-up to [this
comment](https://github.com/zed-industries/zed/discussions/55352#discussioncomment-16961889).
This extracts the buffer header and breadcrumb rendering helpers out of
`element.rs` into a `header.rs` and mouse related things to `mouse.rs`

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 is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A
2026-05-29 11:51:17 +00:00

1055 lines
43 KiB
Rust

use std::path::Path;
use std::rc::Rc;
use collections::HashMap;
use file_icons::FileIcons;
use git::status::FileStatus;
use gpui::{
Action, AnyElement, App, AvailableSpace, Bounds, ClickEvent, ClipboardItem, ContentMask,
CursorStyle, DefiniteLength, Entity, Focusable as _, Hitbox, HitboxBehavior, Hsla, IntoElement,
Length, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels,
ShapedLine, SharedString, Styled, TextAlign, Window, div, fill, linear_color_stop,
linear_gradient, point, px, size,
};
use language::language_settings::ShowWhitespaceSetting;
use multi_buffer::{Anchor, ExcerptBoundaryInfo};
use project::Entry;
use settings::{RelativeLineNumbers, Settings};
use smallvec::SmallVec;
use sum_tree::Bias;
use text::BufferId;
use theme::ActiveTheme;
use ui::{
ButtonLike, ContextMenu, Indicator, KeyBinding, Tooltip, prelude::*, right_click_menu,
text_for_keystroke,
};
use util::ResultExt;
use workspace::{ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel};
use super::{
BlockLayout, EditorElement, EditorLayout, LineWithInvisibles, layout_line,
render_breadcrumb_text,
};
use crate::{
BUFFER_HEADER_PADDING, DisplayRow, Editor, EditorSettings, EditorSnapshot, FILE_HEADER_HEIGHT,
GutterDimensions, JumpData, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, Point, RowExt,
SelectionEffects, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
display_map::ToDisplayPoint,
scroll::{Autoscroll, ScrollOffset, ScrollPixelOffset},
};
pub(crate) struct StickyHeader {
sticky_row: DisplayRow,
pub(crate) start_point: Point,
pub(crate) offset: ScrollOffset,
}
pub(super) struct StickyHeaders {
pub(super) lines: Vec<StickyHeaderLine>,
gutter_background: Hsla,
content_background: Hsla,
gutter_right_padding: Pixels,
}
pub(super) struct StickyHeaderLine {
row: DisplayRow,
pub(super) offset: Pixels,
line: Rc<LineWithInvisibles>,
line_number: Option<ShapedLine>,
elements: SmallVec<[AnyElement; 1]>,
available_text_width: Pixels,
hitbox: Hitbox,
}
impl EditorElement {
pub(crate) fn sticky_headers(editor: &Editor, snapshot: &EditorSnapshot) -> Vec<StickyHeader> {
let scroll_top = snapshot.scroll_position().y;
let mut end_rows = Vec::<DisplayRow>::new();
let mut rows = Vec::<StickyHeader>::new();
for item in editor.sticky_headers.iter().flatten() {
let start_point = item
.source_range_for_text
.start
.to_point(snapshot.buffer_snapshot());
let end_point = item.range.end.to_point(snapshot.buffer_snapshot());
let sticky_row = snapshot
.display_snapshot
.point_to_display_point(start_point, Bias::Left)
.row();
if rows
.last()
.is_some_and(|last| last.sticky_row == sticky_row)
{
continue;
}
let end_row = snapshot
.display_snapshot
.point_to_display_point(end_point, Bias::Left)
.row();
let max_sticky_row = end_row.previous_row();
if max_sticky_row <= sticky_row {
continue;
}
while end_rows
.last()
.is_some_and(|&last_end| last_end <= sticky_row)
{
end_rows.pop();
}
let depth = end_rows.len();
let adjusted_scroll_top = scroll_top + depth as f64;
if sticky_row.as_f64() >= adjusted_scroll_top || end_row.as_f64() <= adjusted_scroll_top
{
continue;
}
let max_scroll_offset = max_sticky_row.as_f64() - scroll_top;
let offset = (depth as f64).min(max_scroll_offset);
end_rows.push(end_row);
rows.push(StickyHeader {
sticky_row,
start_point,
offset,
});
}
rows
}
pub(super) fn should_show_buffer_headers(&self) -> bool {
self.split_side.is_none()
}
pub(super) fn layout_sticky_buffer_header(
&self,
StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>,
scroll_position: gpui::Point<ScrollOffset>,
line_height: Pixels,
right_margin: Pixels,
snapshot: &EditorSnapshot,
hitbox: &Hitbox,
selected_buffer_ids: &Vec<BufferId>,
blocks: &[BlockLayout],
latest_selection_anchors: &HashMap<BufferId, Anchor>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let jump_data = header_jump_data(
snapshot,
DisplayRow(scroll_position.y as u32),
FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
excerpt,
latest_selection_anchors,
);
let editor_bg_color = cx.theme().colors().editor_background;
let selected = selected_buffer_ids.contains(&excerpt.buffer_id());
let available_width = hitbox.bounds.size.width - right_margin;
let mut header = v_flex()
.w_full()
.relative()
.child(
div()
.w(available_width)
.h(FILE_HEADER_HEIGHT as f32 * line_height)
.bg(linear_gradient(
0.,
linear_color_stop(editor_bg_color.opacity(0.), 0.),
linear_color_stop(editor_bg_color, 0.6),
))
.absolute()
.top_0(),
)
.child(
render_buffer_header(
&self.editor,
excerpt,
false,
selected,
true,
jump_data,
window,
cx,
)
.into_any_element(),
)
.into_any_element();
let mut origin = hitbox.origin;
// Move floating header up to avoid colliding with the next buffer header.
for block in blocks.iter() {
if !block.is_buffer_header {
continue;
}
let Some(display_row) = block.row.filter(|row| row.0 > scroll_position.y as u32) else {
continue;
};
let max_row = display_row.0.saturating_sub(FILE_HEADER_HEIGHT);
let offset = scroll_position.y - max_row as f64;
if offset > 0.0 {
origin.y -= Pixels::from(offset * ScrollPixelOffset::from(line_height));
}
break;
}
let size = size(
AvailableSpace::Definite(available_width),
AvailableSpace::MinContent,
);
header.prepaint_as_root(origin, size, window, cx);
header
}
pub(super) fn layout_sticky_headers(
&self,
snapshot: &EditorSnapshot,
editor_width: Pixels,
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
line_height: Pixels,
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
content_origin: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
text_hitbox: &Hitbox,
relative_line_numbers: RelativeLineNumbers,
relative_to: Option<DisplayRow>,
window: &mut Window,
cx: &mut App,
) -> Option<StickyHeaders> {
let show_line_numbers = snapshot
.show_line_numbers
.unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
let rows = Self::sticky_headers(self.editor.read(cx), snapshot);
let mut lines = Vec::<StickyHeaderLine>::new();
for StickyHeader {
sticky_row,
start_point,
offset,
} in rows.into_iter().rev()
{
let line = layout_line(
sticky_row,
snapshot,
&self.style,
editor_width,
is_row_soft_wrapped,
window,
cx,
);
let line_number = show_line_numbers.then(|| {
let start_display_row = start_point.to_display_point(snapshot).row();
let relative_number = relative_to
.filter(|_| relative_line_numbers != RelativeLineNumbers::Disabled)
.map(|base| {
snapshot.relative_line_delta(
base,
start_display_row,
relative_line_numbers == RelativeLineNumbers::Wrapped,
)
});
let number = relative_number
.filter(|&delta| delta != 0)
.map(|delta| delta.unsigned_abs() as u32)
.unwrap_or(start_point.row + 1);
let color = cx.theme().colors().editor_line_number;
self.shape_line_number(SharedString::from(number.to_string()), color, window)
});
lines.push(StickyHeaderLine::new(
sticky_row,
line_height * offset as f32,
line,
line_number,
line_height,
scroll_pixel_position,
content_origin,
gutter_hitbox,
text_hitbox,
window,
cx,
));
}
lines.reverse();
if lines.is_empty() {
return None;
}
Some(StickyHeaders {
lines,
gutter_background: cx.theme().colors().editor_gutter_background,
content_background: self.style.background,
gutter_right_padding: gutter_dimensions.right_padding,
})
}
pub(super) fn paint_sticky_headers(
&mut self,
layout: &mut EditorLayout,
window: &mut Window,
cx: &mut App,
) {
let Some(mut sticky_headers) = layout.sticky_headers.take() else {
return;
};
let Some(last_line_offset) = sticky_headers.lines.last().map(|line| line.offset) else {
layout.sticky_headers = Some(sticky_headers);
return;
};
let whitespace_setting = self
.editor
.read(cx)
.buffer
.read(cx)
.language_settings(cx)
.show_whitespaces;
sticky_headers.paint(layout, whitespace_setting, window, cx);
let sticky_header_hitboxes: Vec<Hitbox> = sticky_headers
.lines
.iter()
.map(|line| line.hitbox.clone())
.collect();
let hovered_hitbox = sticky_header_hitboxes
.iter()
.find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id));
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, _cx| {
if !phase.bubble() {
return;
}
let current_hover = sticky_header_hitboxes
.iter()
.find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id));
if hovered_hitbox != current_hover {
window.refresh();
}
});
let position_map = layout.position_map.clone();
for (line_index, line) in sticky_headers.lines.iter().enumerate() {
let editor = self.editor.clone();
let hitbox = line.hitbox.clone();
let row = line.row;
let line_layout = line.line.clone();
let position_map = position_map.clone();
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if !phase.bubble() {
return;
}
if event.button == MouseButton::Left && hitbox.is_hovered(window) {
let point_for_position =
position_map.point_for_position_on_line(event.position, row, &line_layout);
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let anchor = snapshot
.display_snapshot
.display_point_to_anchor(point_for_position.nearest_valid, Bias::Left);
editor.change_selections(
SelectionEffects::scroll(Autoscroll::top_relative(
line_index as ScrollOffset,
)),
window,
cx,
|selections| {
selections.clear_disjoint();
selections.set_pending_anchor_range(
anchor..anchor,
crate::SelectMode::Character,
);
},
);
cx.stop_propagation();
});
}
});
}
let text_bounds = layout.position_map.text_hitbox.bounds;
let border_top = text_bounds.top() + last_line_offset + layout.position_map.line_height;
let separator_height = px(1.);
let border_bounds = window.pixel_snap_bounds(Bounds::from_corners(
point(layout.gutter_hitbox.bounds.left(), border_top),
point(text_bounds.right(), border_top + separator_height),
));
window.paint_quad(fill(border_bounds, cx.theme().colors().border_variant));
layout.sticky_headers = Some(sticky_headers);
}
}
impl StickyHeaders {
fn paint(
&mut self,
layout: &mut EditorLayout,
whitespace_setting: ShowWhitespaceSetting,
window: &mut Window,
cx: &mut App,
) {
let line_height = layout.position_map.line_height;
for line in self.lines.iter_mut().rev() {
window.paint_layer(
Bounds::new(
layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
size(line.hitbox.size.width, line_height),
),
|window| {
let gutter_bounds = Bounds::new(
layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
size(layout.gutter_hitbox.size.width, line_height),
);
window.paint_quad(fill(gutter_bounds, self.gutter_background));
let text_bounds = Bounds::new(
layout.position_map.text_hitbox.origin + point(Pixels::ZERO, line.offset),
size(line.available_text_width, line_height),
);
window.paint_quad(fill(text_bounds, self.content_background));
if line.hitbox.is_hovered(window) {
let hover_overlay = cx.theme().colors().panel_overlay_hover;
window.paint_quad(fill(gutter_bounds, hover_overlay));
window.paint_quad(fill(text_bounds, hover_overlay));
}
line.paint(
layout,
self.gutter_right_padding,
line.available_text_width,
layout.content_origin,
line_height,
whitespace_setting,
window,
cx,
);
},
);
window.set_cursor_style(CursorStyle::IBeam, &line.hitbox);
}
}
}
impl StickyHeaderLine {
fn new(
row: DisplayRow,
offset: Pixels,
mut line: LineWithInvisibles,
line_number: Option<ShapedLine>,
line_height: Pixels,
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
content_origin: gpui::Point<Pixels>,
gutter_hitbox: &Hitbox,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut App,
) -> Self {
let mut elements = SmallVec::<[AnyElement; 1]>::new();
line.prepaint_with_custom_offset(
line_height,
scroll_pixel_position,
content_origin,
offset,
&mut elements,
window,
cx,
);
let hitbox_bounds = Bounds::new(
gutter_hitbox.origin + point(Pixels::ZERO, offset),
size(text_hitbox.right() - gutter_hitbox.left(), line_height),
);
let available_text_width =
(hitbox_bounds.size.width - gutter_hitbox.size.width).max(Pixels::ZERO);
Self {
row,
offset,
line: Rc::new(line),
line_number,
elements,
available_text_width,
hitbox: window.insert_hitbox(hitbox_bounds, HitboxBehavior::BlockMouseExceptScroll),
}
}
fn paint(
&mut self,
layout: &EditorLayout,
gutter_right_padding: Pixels,
available_text_width: Pixels,
content_origin: gpui::Point<Pixels>,
line_height: Pixels,
whitespace_setting: ShowWhitespaceSetting,
window: &mut Window,
cx: &mut App,
) {
window.with_content_mask(
Some(ContentMask {
bounds: Bounds::new(
layout.position_map.text_hitbox.bounds.origin
+ point(Pixels::ZERO, self.offset),
size(available_text_width, line_height),
),
}),
|window| {
self.line.draw_with_custom_offset(
layout,
self.row,
content_origin,
self.offset,
whitespace_setting,
&[],
window,
cx,
);
for element in &mut self.elements {
element.paint(window, cx);
}
},
);
if let Some(line_number) = &self.line_number {
let gutter_origin = layout.gutter_hitbox.origin + point(Pixels::ZERO, self.offset);
let gutter_width = layout.gutter_hitbox.size.width;
let origin = point(
gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,
gutter_origin.y,
);
line_number
.paint(origin, line_height, TextAlign::Left, None, window, cx)
.log_err();
}
}
}
pub(crate) fn header_jump_data(
editor_snapshot: &EditorSnapshot,
block_row_start: DisplayRow,
height: u32,
first_excerpt: &ExcerptBoundaryInfo,
latest_selection_anchors: &HashMap<BufferId, Anchor>,
) -> JumpData {
let multibuffer_snapshot = editor_snapshot.buffer_snapshot();
let buffer = first_excerpt.buffer(multibuffer_snapshot);
let (jump_anchor, jump_buffer, excerpt_start) = if let Some(anchor) =
latest_selection_anchors.get(&first_excerpt.buffer_id())
&& let Some((jump_anchor, selection_buffer)) =
multibuffer_snapshot.anchor_to_buffer_anchor(*anchor)
{
let jump_offset = text::ToOffset::to_offset(&jump_anchor, selection_buffer);
let selection_excerpt_start = multibuffer_snapshot
.excerpts_for_buffer(jump_anchor.buffer_id)
.find(|excerpt| {
let start = text::ToOffset::to_offset(&excerpt.context.start, selection_buffer);
let end = text::ToOffset::to_offset(&excerpt.context.end, selection_buffer);
start <= jump_offset && jump_offset <= end
})
.map(|excerpt| excerpt.context.start)
.unwrap_or(first_excerpt.range.context.start);
(jump_anchor, selection_buffer, selection_excerpt_start)
} else {
(
first_excerpt.range.primary.start,
buffer,
first_excerpt.range.context.start,
)
};
let jump_position = language::ToPoint::to_point(&jump_anchor, jump_buffer);
let rows_from_excerpt_start = if jump_anchor == excerpt_start {
0
} else {
let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, jump_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(
editor_snapshot
.scroll_anchor
.scroll_position(&editor_snapshot.display_snapshot)
.y as u32,
);
JumpData::MultiBufferPoint {
anchor: jump_anchor,
position: jump_position,
line_offset_from_top,
}
}
pub(crate) fn render_buffer_header(
editor: &Entity<Editor>,
for_excerpt: &ExcerptBoundaryInfo,
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 multibuffer_snapshot = multi_buffer.snapshot(cx);
let buffer = for_excerpt.buffer(&multibuffer_snapshot);
let breadcrumbs = if is_selected {
editor_read.breadcrumbs_inner(cx)
} else {
None
};
let buffer_id = for_excerpt.buffer_id();
let file_status = multi_buffer
.all_diff_hunks_expanded()
.then(|| editor_read.status_for_buffer_id(buffer_id, cx))
.flatten();
let indicator = multi_buffer.buffer(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 = 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 = 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", buffer_id.to_proto()))
.p(BUFFER_HEADER_PADDING)
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
.child(
h_flex()
.group("buffer-header-group")
.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, buffer, 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 = std::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(!buffer.capability.editable(), |el| {
el.child(Icon::new(IconName::FileLock).color(Color::Muted))
})
.when_some(breadcrumbs, |then, breadcrumbs| {
let font = theme_settings::ThemeSettings::get_global(cx)
.buffer_font
.clone();
then.child(render_breadcrumb_text(
breadcrumbs,
Some(font),
None,
editor_handle,
true,
window,
cx,
))
})
},
))
.when(can_open_excerpts && relative_path.is_some(), |this| {
this.child(
div()
.when(!is_selected, |this| {
this.visible_on_hover("buffer-header-group")
})
.child(
Button::new("open-file-button", "Open File")
.style(ButtonStyle::OutlinedGhost)
.when(is_selected, |this| {
this.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 = buffer.file().cloned();
let editor = editor.clone();
let buffer_snapshot = buffer.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();
let buffer_snapshot = buffer_snapshot.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 = editor.update(cx, |editor, cx| {
let mut menu = menu;
for addon in editor.addons.values() {
menu = addon.extend_buffer_header_context_menu(
menu,
&buffer_snapshot,
window,
cx,
);
}
menu
});
menu.context(menu_context)
})
})
}
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
}
})
}