Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:36:18.469Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

header.rs

1108 lines · 46.2 KB · rust
1use std::path::Path;
2use std::rc::Rc;
3
4use collections::HashMap;
5use file_icons::FileIcons;
6use git::status::FileStatus;
7use gpui::{
8    Action, AnyElement, App, AvailableSpace, Bounds, ClickEvent, ClipboardItem, ContentMask,
9    CursorStyle, DefiniteLength, Entity, Focusable as _, Hitbox, HitboxBehavior, Hsla, IntoElement,
10    Length, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels,
11    ShapedLine, SharedString, Styled, TextAlign, Window, WindowBackgroundAppearance, div, fill,
12    linear_color_stop, linear_gradient, point, px, size,
13};
14use language::language_settings::ShowWhitespaceSetting;
15use multi_buffer::{Anchor, ExcerptBoundaryInfo, MultiBuffer};
16use project::Entry;
17use settings::{RelativeLineNumbers, Settings};
18use smallvec::SmallVec;
19use sum_tree::Bias;
20use text::BufferId;
21use theme::ActiveTheme;
22use ui::{
23    ButtonLike, ContextMenu, DiffStat, Indicator, KeyBinding, Tooltip, prelude::*,
24    right_click_menu, text_for_keystroke, utils::WithRemSize,
25};
26use util::ResultExt;
27use workspace::{ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel};
28
29use super::{
30    BlockLayout, EditorElement, EditorLayout, LineWithInvisibles, layout_line,
31    render_breadcrumb_text,
32};
33use crate::{
34    BUFFER_HEADER_PADDING, DisplayRow, Editor, EditorSettings, EditorSnapshot, FILE_HEADER_HEIGHT,
35    GutterDimensions, JumpData, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, Point, RowExt,
36    SelectionEffects, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
37    display_map::ToDisplayPoint,
38    scroll::{Autoscroll, ScrollOffset, ScrollPixelOffset},
39};
40
41pub(crate) struct StickyHeader {
42    sticky_row: DisplayRow,
43    pub(crate) start_point: Point,
44    pub(crate) offset: ScrollOffset,
45}
46
47pub(super) struct StickyHeaders {
48    pub(super) lines: Vec<StickyHeaderLine>,
49    gutter_background: Hsla,
50    content_background: Hsla,
51    gutter_right_padding: Pixels,
52}
53
54pub(super) struct StickyHeaderLine {
55    row: DisplayRow,
56    pub(super) offset: Pixels,
57    line: Rc<LineWithInvisibles>,
58    line_number: Option<ShapedLine>,
59    elements: SmallVec<[AnyElement; 1]>,
60    available_text_width: Pixels,
61    hitbox: Hitbox,
62}
63
64impl EditorElement {
65    pub(crate) fn sticky_headers(editor: &Editor, snapshot: &EditorSnapshot) -> Vec<StickyHeader> {
66        let scroll_top = snapshot.scroll_position().y;
67
68        let mut end_rows = Vec::<DisplayRow>::new();
69        let mut rows = Vec::<StickyHeader>::new();
70
71        for item in editor.sticky_headers.iter().flatten() {
72            let selection_start = item
73                .selection_range
74                .start
75                .to_point(snapshot.buffer_snapshot());
76            let source_text_start = item
77                .source_range_for_text
78                .start
79                .to_point(snapshot.buffer_snapshot());
80            let start_column = if source_text_start.row == selection_start.row {
81                source_text_start.column
82            } else {
83                0
84            };
85            let start_point = Point::new(selection_start.row, start_column);
86            let end_point = item.range.end.to_point(snapshot.buffer_snapshot());
87
88            let sticky_row = snapshot
89                .display_snapshot
90                .point_to_display_point(start_point, Bias::Left)
91                .row();
92            if rows
93                .last()
94                .is_some_and(|last| last.sticky_row == sticky_row)
95            {
96                continue;
97            }
98
99            let end_row = snapshot
100                .display_snapshot
101                .point_to_display_point(end_point, Bias::Left)
102                .row();
103            let max_sticky_row = end_row.previous_row();
104            if max_sticky_row <= sticky_row {
105                continue;
106            }
107
108            while end_rows
109                .last()
110                .is_some_and(|&last_end| last_end <= sticky_row)
111            {
112                end_rows.pop();
113            }
114            let depth = end_rows.len();
115            let adjusted_scroll_top = scroll_top + depth as f64;
116
117            if sticky_row.as_f64() >= adjusted_scroll_top || end_row.as_f64() <= adjusted_scroll_top
118            {
119                continue;
120            }
121
122            let max_scroll_offset = max_sticky_row.as_f64() - scroll_top;
123            let offset = (depth as f64).min(max_scroll_offset);
124
125            end_rows.push(end_row);
126            rows.push(StickyHeader {
127                sticky_row,
128                start_point,
129                offset,
130            });
131        }
132
133        rows
134    }
135
136    pub(super) fn should_show_buffer_headers(&self) -> bool {
137        self.split_side.is_none()
138    }
139
140    pub(super) fn layout_sticky_buffer_header(
141        &self,
142        StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>,
143        scroll_position: gpui::Point<ScrollOffset>,
144        line_height: Pixels,
145        right_margin: Pixels,
146        snapshot: &EditorSnapshot,
147        hitbox: &Hitbox,
148        selected_buffer_ids: &Vec<BufferId>,
149        blocks: &[BlockLayout],
150        latest_selection_anchors: &HashMap<BufferId, Anchor>,
151        window: &mut Window,
152        cx: &mut App,
153    ) -> AnyElement {
154        let jump_data = header_jump_data(
155            snapshot,
156            DisplayRow(scroll_position.y as u32),
157            FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
158            excerpt,
159            latest_selection_anchors,
160        );
161
162        let editor_bg_color = cx.theme().colors().editor_background;
163
164        let selected = selected_buffer_ids.contains(&excerpt.buffer_id());
165
166        let available_width = hitbox.bounds.size.width - right_margin;
167
168        let mut header = v_flex()
169            .w_full()
170            .relative()
171            .child(
172                div()
173                    .w(available_width)
174                    .h(FILE_HEADER_HEIGHT as f32 * line_height)
175                    .bg(linear_gradient(
176                        0.,
177                        linear_color_stop(editor_bg_color.opacity(0.), 0.),
178                        linear_color_stop(editor_bg_color, 0.6),
179                    ))
180                    .absolute()
181                    .top_0(),
182            )
183            .child(
184                render_buffer_header(
185                    &self.editor,
186                    excerpt,
187                    false,
188                    selected,
189                    true,
190                    jump_data,
191                    window,
192                    cx,
193                )
194                .into_any_element(),
195            )
196            .into_any_element();
197
198        let mut origin = hitbox.origin;
199        // Move floating header up to avoid colliding with the next buffer header.
200        for block in blocks.iter() {
201            if !block.is_buffer_header {
202                continue;
203            }
204
205            let Some(display_row) = block.row.filter(|row| row.0 > scroll_position.y as u32) else {
206                continue;
207            };
208
209            let max_row = display_row.0.saturating_sub(FILE_HEADER_HEIGHT);
210            let offset = scroll_position.y - max_row as f64;
211
212            if offset > 0.0 {
213                origin.y -= Pixels::from(offset * ScrollPixelOffset::from(line_height));
214            }
215            break;
216        }
217
218        let size = size(
219            AvailableSpace::Definite(available_width),
220            AvailableSpace::MinContent,
221        );
222
223        header.prepaint_as_root(origin, size, window, cx);
224
225        header
226    }
227
228    pub(super) fn layout_sticky_headers(
229        &self,
230        snapshot: &EditorSnapshot,
231        editor_width: Pixels,
232        is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
233        line_height: Pixels,
234        scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
235        content_origin: gpui::Point<Pixels>,
236        gutter_dimensions: &GutterDimensions,
237        gutter_hitbox: &Hitbox,
238        text_hitbox: &Hitbox,
239        relative_line_numbers: RelativeLineNumbers,
240        relative_to: Option<DisplayRow>,
241        window: &mut Window,
242        cx: &mut App,
243    ) -> Option<StickyHeaders> {
244        let show_line_numbers = snapshot
245            .show_line_numbers
246            .unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
247
248        let rows = Self::sticky_headers(self.editor.read(cx), snapshot);
249
250        let mut lines = Vec::<StickyHeaderLine>::new();
251
252        for StickyHeader {
253            sticky_row,
254            start_point,
255            offset,
256        } in rows.into_iter().rev()
257        {
258            let line = layout_line(
259                sticky_row,
260                snapshot,
261                &self.style,
262                editor_width,
263                is_row_soft_wrapped,
264                window,
265                cx,
266            );
267
268            let line_number = show_line_numbers.then(|| {
269                let start_display_row = start_point.to_display_point(snapshot).row();
270                let relative_number = relative_to
271                    .filter(|_| relative_line_numbers != RelativeLineNumbers::Disabled)
272                    .map(|base| {
273                        snapshot.relative_line_delta(
274                            base,
275                            start_display_row,
276                            relative_line_numbers == RelativeLineNumbers::Wrapped,
277                        )
278                    });
279                let number = relative_number
280                    .filter(|&delta| delta != 0)
281                    .map(|delta| delta.unsigned_abs() as u32)
282                    .unwrap_or(start_point.row + 1);
283                let color = cx.theme().colors().editor_line_number;
284                self.shape_line_number(SharedString::from(number.to_string()), color, window)
285            });
286
287            lines.push(StickyHeaderLine::new(
288                sticky_row,
289                line_height * offset as f32,
290                line,
291                line_number,
292                line_height,
293                scroll_pixel_position,
294                content_origin,
295                gutter_hitbox,
296                text_hitbox,
297                window,
298                cx,
299            ));
300        }
301
302        lines.reverse();
303        if lines.is_empty() {
304            return None;
305        }
306
307        Some(StickyHeaders {
308            lines,
309            gutter_background: cx.theme().colors().editor_gutter_background,
310            content_background: self.style.background,
311            gutter_right_padding: gutter_dimensions.right_padding,
312        })
313    }
314
315    pub(super) fn paint_sticky_headers(
316        &mut self,
317        layout: &mut EditorLayout,
318        window: &mut Window,
319        cx: &mut App,
320    ) {
321        let Some(mut sticky_headers) = layout.sticky_headers.take() else {
322            return;
323        };
324
325        let Some(last_line_offset) = sticky_headers.lines.last().map(|line| line.offset) else {
326            layout.sticky_headers = Some(sticky_headers);
327            return;
328        };
329
330        let whitespace_setting = self
331            .editor
332            .read(cx)
333            .buffer
334            .read(cx)
335            .language_settings(cx)
336            .show_whitespaces;
337        sticky_headers.paint(layout, whitespace_setting, window, cx);
338
339        let sticky_header_hitboxes: Vec<Hitbox> = sticky_headers
340            .lines
341            .iter()
342            .map(|line| line.hitbox.clone())
343            .collect();
344        let hovered_hitbox = sticky_header_hitboxes
345            .iter()
346            .find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id));
347
348        window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, _cx| {
349            if !phase.bubble() {
350                return;
351            }
352
353            let current_hover = sticky_header_hitboxes
354                .iter()
355                .find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id));
356            if hovered_hitbox != current_hover {
357                window.refresh();
358            }
359        });
360
361        let position_map = layout.position_map.clone();
362
363        for (line_index, line) in sticky_headers.lines.iter().enumerate() {
364            let editor = self.editor.clone();
365            let hitbox = line.hitbox.clone();
366            let row = line.row;
367            let line_layout = line.line.clone();
368            let position_map = position_map.clone();
369            window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
370                if !phase.bubble() {
371                    return;
372                }
373
374                if event.button == MouseButton::Left && hitbox.is_hovered(window) {
375                    let point_for_position =
376                        position_map.point_for_position_on_line(event.position, row, &line_layout);
377
378                    editor.update(cx, |editor, cx| {
379                        let snapshot = editor.snapshot(window, cx);
380                        let anchor = snapshot
381                            .display_snapshot
382                            .display_point_to_anchor(point_for_position.nearest_valid, Bias::Left);
383                        editor.change_selections(
384                            SelectionEffects::scroll(Autoscroll::top_relative(
385                                line_index as ScrollOffset,
386                            )),
387                            window,
388                            cx,
389                            |selections| {
390                                selections.clear_disjoint();
391                                selections.set_pending_anchor_range(
392                                    anchor..anchor,
393                                    crate::SelectMode::Character,
394                                );
395                            },
396                        );
397                        cx.stop_propagation();
398                    });
399                }
400            });
401        }
402
403        let text_bounds = layout.position_map.text_hitbox.bounds;
404        let border_top = text_bounds.top() + last_line_offset + layout.position_map.line_height;
405        let separator_height = px(1.);
406        let border_bounds = window.pixel_snap_bounds(Bounds::from_corners(
407            point(layout.gutter_hitbox.bounds.left(), border_top),
408            point(text_bounds.right(), border_top + separator_height),
409        ));
410        window.paint_quad(fill(border_bounds, cx.theme().colors().border_variant));
411
412        layout.sticky_headers = Some(sticky_headers);
413    }
414}
415
416impl StickyHeaders {
417    fn paint(
418        &mut self,
419        layout: &mut EditorLayout,
420        whitespace_setting: ShowWhitespaceSetting,
421        window: &mut Window,
422        cx: &mut App,
423    ) {
424        let line_height = layout.position_map.line_height;
425
426        for line in self.lines.iter_mut().rev() {
427            window.paint_layer(
428                Bounds::new(
429                    layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
430                    size(line.hitbox.size.width, line_height),
431                ),
432                |window| {
433                    let gutter_bounds = Bounds::new(
434                        layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
435                        size(layout.gutter_hitbox.size.width, line_height),
436                    );
437                    window.paint_quad(fill(gutter_bounds, self.gutter_background));
438
439                    let text_bounds = Bounds::new(
440                        layout.position_map.text_hitbox.origin + point(Pixels::ZERO, line.offset),
441                        size(line.available_text_width, line_height),
442                    );
443                    window.paint_quad(fill(text_bounds, self.content_background));
444
445                    if line.hitbox.is_hovered(window) {
446                        let hover_overlay = cx.theme().colors().panel_overlay_hover;
447                        window.paint_quad(fill(gutter_bounds, hover_overlay));
448                        window.paint_quad(fill(text_bounds, hover_overlay));
449                    }
450
451                    line.paint(
452                        layout,
453                        self.gutter_right_padding,
454                        line.available_text_width,
455                        layout.content_origin,
456                        line_height,
457                        whitespace_setting,
458                        window,
459                        cx,
460                    );
461                },
462            );
463
464            window.set_cursor_style(CursorStyle::IBeam, &line.hitbox);
465        }
466    }
467}
468
469impl StickyHeaderLine {
470    fn new(
471        row: DisplayRow,
472        offset: Pixels,
473        mut line: LineWithInvisibles,
474        line_number: Option<ShapedLine>,
475        line_height: Pixels,
476        scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
477        content_origin: gpui::Point<Pixels>,
478        gutter_hitbox: &Hitbox,
479        text_hitbox: &Hitbox,
480        window: &mut Window,
481        cx: &mut App,
482    ) -> Self {
483        let mut elements = SmallVec::<[AnyElement; 1]>::new();
484        line.prepaint_with_custom_offset(
485            line_height,
486            scroll_pixel_position,
487            content_origin,
488            offset,
489            &mut elements,
490            window,
491            cx,
492        );
493
494        let hitbox_bounds = Bounds::new(
495            gutter_hitbox.origin + point(Pixels::ZERO, offset),
496            size(text_hitbox.right() - gutter_hitbox.left(), line_height),
497        );
498        let available_text_width =
499            (hitbox_bounds.size.width - gutter_hitbox.size.width).max(Pixels::ZERO);
500
501        Self {
502            row,
503            offset,
504            line: Rc::new(line),
505            line_number,
506            elements,
507            available_text_width,
508            hitbox: window.insert_hitbox(hitbox_bounds, HitboxBehavior::BlockMouseExceptScroll),
509        }
510    }
511
512    fn paint(
513        &mut self,
514        layout: &EditorLayout,
515        gutter_right_padding: Pixels,
516        available_text_width: Pixels,
517        content_origin: gpui::Point<Pixels>,
518        line_height: Pixels,
519        whitespace_setting: ShowWhitespaceSetting,
520        window: &mut Window,
521        cx: &mut App,
522    ) {
523        window.with_content_mask(
524            Some(ContentMask {
525                bounds: Bounds::new(
526                    layout.position_map.text_hitbox.bounds.origin
527                        + point(Pixels::ZERO, self.offset),
528                    size(available_text_width, line_height),
529                ),
530            }),
531            |window| {
532                self.line.draw_with_custom_offset(
533                    layout,
534                    self.row,
535                    content_origin,
536                    self.offset,
537                    whitespace_setting,
538                    &[],
539                    window,
540                    cx,
541                );
542                for element in &mut self.elements {
543                    element.paint(window, cx);
544                }
545            },
546        );
547
548        if let Some(line_number) = &self.line_number {
549            let gutter_origin = layout.gutter_hitbox.origin + point(Pixels::ZERO, self.offset);
550            let gutter_width = layout.gutter_hitbox.size.width;
551            let origin = point(
552                gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,
553                gutter_origin.y,
554            );
555            line_number
556                .paint(origin, line_height, TextAlign::Left, None, window, cx)
557                .log_err();
558        }
559    }
560}
561
562pub(crate) fn header_jump_data(
563    editor_snapshot: &EditorSnapshot,
564    block_row_start: DisplayRow,
565    height: u32,
566    first_excerpt: &ExcerptBoundaryInfo,
567    latest_selection_anchors: &HashMap<BufferId, Anchor>,
568) -> JumpData {
569    let multibuffer_snapshot = editor_snapshot.buffer_snapshot();
570    let buffer = first_excerpt.buffer(multibuffer_snapshot);
571    let (jump_anchor, jump_buffer, excerpt_start) = if let Some(anchor) =
572        latest_selection_anchors.get(&first_excerpt.buffer_id())
573        && let Some((jump_anchor, selection_buffer)) =
574            multibuffer_snapshot.anchor_to_buffer_anchor(*anchor)
575    {
576        let jump_offset = text::ToOffset::to_offset(&jump_anchor, selection_buffer);
577        let selection_excerpt_start = multibuffer_snapshot
578            .excerpts_for_buffer(jump_anchor.buffer_id)
579            .find(|excerpt| {
580                let start = text::ToOffset::to_offset(&excerpt.context.start, selection_buffer);
581                let end = text::ToOffset::to_offset(&excerpt.context.end, selection_buffer);
582                start <= jump_offset && jump_offset <= end
583            })
584            .map(|excerpt| excerpt.context.start)
585            .unwrap_or(first_excerpt.range.context.start);
586        (jump_anchor, selection_buffer, selection_excerpt_start)
587    } else {
588        (
589            first_excerpt.range.primary.start,
590            buffer,
591            first_excerpt.range.context.start,
592        )
593    };
594    let jump_position = language::ToPoint::to_point(&jump_anchor, jump_buffer);
595    let rows_from_excerpt_start = if jump_anchor == excerpt_start {
596        0
597    } else {
598        let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, jump_buffer);
599        jump_position.row.saturating_sub(excerpt_start_point.row)
600    };
601
602    let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start)
603        .saturating_sub(
604            editor_snapshot
605                .scroll_anchor
606                .scroll_position(&editor_snapshot.display_snapshot)
607                .y as u32,
608        );
609
610    JumpData::MultiBufferPoint {
611        anchor: jump_anchor,
612        position: jump_position,
613        line_offset_from_top,
614    }
615}
616
617pub(crate) fn render_buffer_header(
618    editor: &Entity<Editor>,
619    for_excerpt: &ExcerptBoundaryInfo,
620    is_folded: bool,
621    is_selected: bool,
622    is_sticky: bool,
623    jump_data: JumpData,
624    window: &mut Window,
625    cx: &mut App,
626) -> impl IntoElement {
627    let buffer_id = for_excerpt.buffer_id();
628    let header_hovered_state = window.use_keyed_state(
629        ("buffer-header-hovered", buffer_id.to_proto()),
630        cx,
631        |_, _| false,
632    );
633    let header_hovered = *header_hovered_state.read(cx);
634    let editor_read = editor.read(cx);
635    let multi_buffer = editor_read.buffer.read(cx);
636    let is_read_only = editor_read.read_only(cx);
637    let editor_handle: &dyn ItemHandle = editor;
638    let multibuffer_snapshot = multi_buffer.snapshot(cx);
639    let buffer = for_excerpt.buffer(&multibuffer_snapshot);
640
641    let breadcrumbs = if is_selected {
642        editor_read.breadcrumbs_inner(cx)
643    } else {
644        None
645    };
646
647    let file_status = multi_buffer
648        .all_diff_hunks_expanded()
649        .then(|| editor_read.status_for_buffer_id(buffer_id, cx))
650        .flatten();
651    let diff_stat = multi_buffer
652        .all_diff_hunks_expanded()
653        .then(|| multibuffer_snapshot.diff_for_buffer_id(buffer_id))
654        .flatten()
655        .map(|diff| diff.changed_row_counts())
656        .filter(|(added, removed)| *added > 0 || *removed > 0);
657    let indicator = multi_buffer.buffer(buffer_id).and_then(|buffer| {
658        let buffer = buffer.read(cx);
659        let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) {
660            (true, _) => Some(Color::Warning),
661            (_, true) => Some(Color::Accent),
662            (false, false) => None,
663        };
664        indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color))
665    });
666
667    let include_root = editor_read
668        .project
669        .as_ref()
670        .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
671        .unwrap_or_default();
672    let file = buffer.file();
673    let can_open_excerpts = file.is_none_or(|file| file.can_open());
674    let path_style = file.map(|file| file.path_style(cx));
675    let relative_path = buffer.resolve_file_path(include_root, cx);
676    let (parent_path, filename) = if let Some(path) = &relative_path {
677        if let Some(path_style) = path_style {
678            let (dir, file_name) = path_style.split(path);
679            (dir.map(|dir| dir.to_owned()), Some(file_name.to_owned()))
680        } else {
681            (None, Some(path.clone()))
682        }
683    } else {
684        (None, None)
685    };
686    let focus_handle = editor_read.focus_handle(cx);
687    let colors = cx.theme().colors();
688    // On transparent windows, only render an opaque `editor_subheader_background` so it masks
689    // the editor content beneath it without creating a darker bar. Sticky shadows still require
690    // an opaque window to avoid rendering as a halo.
691    let opaque_window =
692        cx.theme().window_background_appearance() == WindowBackgroundAppearance::Opaque;
693    let show_header_background = opaque_window || colors.editor_subheader_background.is_opaque();
694
695    let show_open_file_button =
696        can_open_excerpts && relative_path.is_some() && (is_selected || header_hovered);
697
698    let header = div()
699        .id(("buffer-header", buffer_id.to_proto()))
700        .on_hover(move |hovered, _window, cx| {
701            header_hovered_state.update(cx, |state, cx| {
702                if *state != *hovered {
703                    *state = *hovered;
704                    cx.notify();
705                }
706            });
707        })
708        .p(BUFFER_HEADER_PADDING)
709        .w_full()
710        .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
711        .child(
712            h_flex()
713                .group("buffer-header-group")
714                .size_full()
715                .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
716                .pl_1()
717                .pr_2()
718                .rounded_sm()
719                .gap_1p5()
720                .border_1()
721                .map(|border| {
722                    let border_color =
723                        if is_selected && is_folded && focus_handle.contains_focused(window, cx) {
724                            colors.border_focused
725                        } else {
726                            colors.border
727                        };
728                    border.border_color(border_color)
729                })
730                .when(is_sticky && opaque_window, |s| s.shadow_md())
731                .when(show_header_background, |s| {
732                    s.bg(colors.editor_subheader_background)
733                })
734                .hover(|s| s.bg(colors.element_hover))
735                .map(|header| {
736                    let editor = editor.clone();
737                    let buffer_id = for_excerpt.buffer_id();
738                    let toggle_chevron_icon =
739                        FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
740                    let button_size = rems_from_px(28.);
741
742                    header.child(
743                        div()
744                            .hover(|style| style.bg(colors.element_selected))
745                            .rounded_xs()
746                            .child(
747                                ButtonLike::new("toggle-buffer-fold")
748                                    .style(ButtonStyle::Transparent)
749                                    .height(button_size.into())
750                                    .width(button_size)
751                                    .children(toggle_chevron_icon)
752                                    .tooltip({
753                                        let focus_handle = focus_handle.clone();
754                                        let is_folded_for_tooltip = is_folded;
755                                        move |_window, cx| {
756                                            Tooltip::with_meta_in(
757                                                if is_folded_for_tooltip {
758                                                    "Unfold Excerpt"
759                                                } else {
760                                                    "Fold Excerpt"
761                                                },
762                                                Some(&ToggleFold),
763                                                format!(
764                                                    "{} to toggle all",
765                                                    text_for_keystroke(
766                                                        &Modifiers::alt(),
767                                                        "click",
768                                                        cx
769                                                    )
770                                                ),
771                                                &focus_handle,
772                                                cx,
773                                            )
774                                        }
775                                    })
776                                    .on_click(move |event, window, cx| {
777                                        if event.modifiers().alt {
778                                            editor.update(cx, |editor, cx| {
779                                                editor.toggle_fold_all(&ToggleFoldAll, window, cx);
780                                            });
781                                        } else {
782                                            if is_folded {
783                                                editor.update(cx, |editor, cx| {
784                                                    editor.unfold_buffer(buffer_id, cx);
785                                                });
786                                            } else {
787                                                editor.update(cx, |editor, cx| {
788                                                    editor.fold_buffer(buffer_id, cx);
789                                                });
790                                            }
791                                        }
792                                    }),
793                            ),
794                    )
795                })
796                .children(
797                    editor_read
798                        .addons
799                        .values()
800                        .filter_map(|addon| {
801                            addon.render_buffer_header_controls(for_excerpt, buffer, window, cx)
802                        })
803                        .take(1),
804                )
805                .when(!is_read_only, |this| {
806                    this.child(
807                        h_flex()
808                            .size_3()
809                            .justify_center()
810                            .flex_shrink_0()
811                            .children(indicator),
812                    )
813                })
814                .child(
815                    h_flex()
816                        .cursor_pointer()
817                        .id("path_header_block")
818                        .min_w_0()
819                        .size_full()
820                        .gap_1()
821                        .justify_between()
822                        .overflow_hidden()
823                        .child(h_flex().min_w_0().flex_1().gap_0p5().overflow_hidden().map(
824                            |path_header| {
825                                let filename = filename
826                                    .map(SharedString::from)
827                                    .unwrap_or_else(|| MultiBuffer::DEFAULT_TITLE.into());
828
829                                let full_path = match parent_path.as_deref() {
830                                    Some(parent) if !parent.is_empty() => {
831                                        format!("{}{}", parent, filename.as_str())
832                                    }
833                                    _ => filename.as_str().to_string(),
834                                };
835
836                                path_header
837                                    .child(
838                                        ButtonLike::new("filename-button")
839                                            .when(ItemSettings::get_global(cx).file_icons, |this| {
840                                                let path = std::path::Path::new(filename.as_str());
841                                                let icon = FileIcons::get_icon(path, cx)
842                                                    .unwrap_or_default();
843
844                                                this.child(
845                                                    Icon::from_path(icon).color(Color::Muted),
846                                                )
847                                            })
848                                            .child(
849                                                Label::new(filename)
850                                                    .single_line()
851                                                    .color(file_status_label_color(file_status))
852                                                    .buffer_font(cx)
853                                                    .when(
854                                                        file_status.is_some_and(|s| s.is_deleted()),
855                                                        |label| label.strikethrough(),
856                                                    ),
857                                            )
858                                            .tooltip(move |_, cx| {
859                                                Tooltip::with_meta(
860                                                    "Open File",
861                                                    None,
862                                                    full_path.clone(),
863                                                    cx,
864                                                )
865                                            })
866                                            .on_click(window.listener_for(editor, {
867                                                let jump_data = jump_data.clone();
868                                                move |editor, e: &ClickEvent, window, cx| {
869                                                    editor.open_excerpts_common(
870                                                        Some(jump_data.clone()),
871                                                        e.modifiers().secondary(),
872                                                        window,
873                                                        cx,
874                                                    );
875                                                }
876                                            })),
877                                    )
878                                    .when_some(parent_path, |then, path| {
879                                        then.child(
880                                            Label::new(path)
881                                                .buffer_font(cx)
882                                                .truncate_start()
883                                                .color(
884                                                    if file_status
885                                                        .is_some_and(FileStatus::is_deleted)
886                                                    {
887                                                        Color::Custom(colors.text_disabled)
888                                                    } else {
889                                                        Color::Custom(colors.text_muted)
890                                                    },
891                                                ),
892                                        )
893                                    })
894                                    .when(!buffer.capability.editable(), |el| {
895                                        el.child(Icon::new(IconName::FileLock).color(Color::Muted))
896                                    })
897                                    .when_some(breadcrumbs, |then, breadcrumbs| {
898                                        let font = theme_settings::ThemeSettings::get_global(cx)
899                                            .buffer_font
900                                            .clone();
901                                        then.child(render_breadcrumb_text(
902                                            breadcrumbs,
903                                            Some(font),
904                                            None,
905                                            editor_handle,
906                                            true,
907                                            window,
908                                            cx,
909                                        ))
910                                    })
911                            },
912                        ))
913                        .child(
914                            h_flex()
915                                .gap_2()
916                                .when_some(diff_stat, |this, (added, removed)| {
917                                    let ui_font_size =
918                                        theme_settings::ThemeSettings::get_global(cx)
919                                            .ui_font_size(cx);
920                                    this.child(WithRemSize::new(ui_font_size).child(DiffStat::new(
921                                        ("buffer-header-diff-stat", buffer_id.to_proto()),
922                                        added as usize,
923                                        removed as usize,
924                                    )))
925                                })
926                                .when(show_open_file_button, |this| {
927                                    this.child(
928                                        Button::new("open-file-button", "Open File")
929                                            .style(ButtonStyle::OutlinedCustom(
930                                                cx.theme().colors().border.opacity(0.6),
931                                            ))
932                                            .layer(ui::ElevationIndex::ElevatedSurface)
933                                            .when(is_selected, |this| {
934                                                this.key_binding(KeyBinding::for_action_in(
935                                                    &OpenExcerpts,
936                                                    &focus_handle,
937                                                    cx,
938                                                ))
939                                            })
940                                            .on_click(window.listener_for(editor, {
941                                                let jump_data = jump_data.clone();
942                                                move |editor, e: &ClickEvent, window, cx| {
943                                                    editor.open_excerpts_common(
944                                                        Some(jump_data.clone()),
945                                                        e.modifiers().secondary(),
946                                                        window,
947                                                        cx,
948                                                    );
949                                                }
950                                            })),
951                                    )
952                                }),
953                        )
954                        .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
955                        .on_click(window.listener_for(editor, {
956                            let buffer_id = for_excerpt.buffer_id();
957                            move |editor, e: &ClickEvent, window, cx| {
958                                if e.modifiers().alt {
959                                    editor.open_excerpts_common(
960                                        Some(jump_data.clone()),
961                                        e.modifiers().secondary(),
962                                        window,
963                                        cx,
964                                    );
965                                    return;
966                                }
967
968                                if is_folded {
969                                    editor.unfold_buffer(buffer_id, cx);
970                                } else {
971                                    editor.fold_buffer(buffer_id, cx);
972                                }
973                            }
974                        })),
975                ),
976        );
977
978    let file = buffer.file().cloned();
979    let editor = editor.clone();
980    let buffer_snapshot = buffer.clone();
981
982    right_click_menu(("buffer-header-context-menu", buffer_id.to_proto()))
983        .trigger(move |_, _, _| header)
984        .menu(move |window, cx| {
985            let menu_context = focus_handle.clone();
986            let editor = editor.clone();
987            let file = file.clone();
988            let buffer_snapshot = buffer_snapshot.clone();
989            ContextMenu::build(window, cx, move |mut menu, window, cx| {
990                if let Some(file) = file
991                    && let Some(project) = editor.read(cx).project()
992                    && let Some(worktree) =
993                        project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
994                {
995                    let path_style = file.path_style(cx);
996                    let worktree = worktree.read(cx);
997                    let relative_path = file.path();
998                    let entry_for_path = worktree.entry_for_path(relative_path);
999                    let abs_path = entry_for_path.map(|e| {
1000                        e.canonical_path
1001                            .as_deref()
1002                            .map_or_else(|| worktree.absolutize(relative_path), Path::to_path_buf)
1003                    });
1004                    let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
1005
1006                    let parent_abs_path = abs_path
1007                        .as_ref()
1008                        .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
1009                    let relative_path = has_relative_path
1010                        .then_some(relative_path)
1011                        .map(ToOwned::to_owned);
1012
1013                    let visible_in_project_panel = relative_path.is_some() && worktree.is_visible();
1014                    let reveal_in_project_panel = entry_for_path
1015                        .filter(|_| visible_in_project_panel)
1016                        .map(|entry| entry.id);
1017                    menu = menu
1018                        .when_some(abs_path, |menu, abs_path| {
1019                            menu.entry(
1020                                "Copy Path",
1021                                Some(Box::new(zed_actions::workspace::CopyPath)),
1022                                window.handler_for(&editor, move |_, _, cx| {
1023                                    cx.write_to_clipboard(ClipboardItem::new_string(
1024                                        abs_path.to_string_lossy().into_owned(),
1025                                    ));
1026                                }),
1027                            )
1028                        })
1029                        .when_some(relative_path, |menu, relative_path| {
1030                            menu.entry(
1031                                "Copy Relative Path",
1032                                Some(Box::new(zed_actions::workspace::CopyRelativePath)),
1033                                window.handler_for(&editor, move |_, _, cx| {
1034                                    cx.write_to_clipboard(ClipboardItem::new_string(
1035                                        relative_path.display(path_style).to_string(),
1036                                    ));
1037                                }),
1038                            )
1039                        })
1040                        .when(
1041                            reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
1042                            |menu| menu.separator(),
1043                        )
1044                        .when_some(reveal_in_project_panel, |menu, entry_id| {
1045                            menu.entry(
1046                                "Reveal In Project Panel",
1047                                Some(Box::new(RevealInProjectPanel::default())),
1048                                window.handler_for(&editor, move |editor, _, cx| {
1049                                    if let Some(project) = &mut editor.project {
1050                                        project.update(cx, |_, cx| {
1051                                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
1052                                        });
1053                                    }
1054                                }),
1055                            )
1056                        })
1057                        .when_some(parent_abs_path, |menu, parent_abs_path| {
1058                            menu.entry(
1059                                "Open in Terminal",
1060                                Some(Box::new(OpenInTerminal)),
1061                                window.handler_for(&editor, move |_, window, cx| {
1062                                    window.dispatch_action(
1063                                        OpenTerminal {
1064                                            working_directory: parent_abs_path.clone(),
1065                                            local: false,
1066                                        }
1067                                        .boxed_clone(),
1068                                        cx,
1069                                    );
1070                                }),
1071                            )
1072                        });
1073                }
1074
1075                menu = editor.update(cx, |editor, cx| {
1076                    let mut menu = menu;
1077                    for addon in editor.addons.values() {
1078                        menu = addon.extend_buffer_header_context_menu(
1079                            menu,
1080                            &buffer_snapshot,
1081                            window,
1082                            cx,
1083                        );
1084                    }
1085                    menu
1086                });
1087
1088                menu.context(menu_context)
1089            })
1090        })
1091}
1092
1093pub fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
1094    file_status.map_or(Color::Default, |status| {
1095        if status.is_conflicted() {
1096            Color::Conflict
1097        } else if status.is_modified() {
1098            Color::Modified
1099        } else if status.is_deleted() {
1100            Color::Disabled
1101        } else if status.is_created() {
1102            Color::Created
1103        } else {
1104            Color::Default
1105        }
1106    })
1107}
1108
Served at tenant.openagents/omega Member data and write actions are omitted.