split_editor_view.rs

   1use std::{cmp, collections::HashMap, path, path::Path};
   2
   3use collections::HashSet;
   4use file_icons::FileIcons;
   5use git::status::FileStatus;
   6use gpui::{
   7    AbsoluteLength, Action, AnyElement, App, AvailableSpace, Bounds, ClickEvent, ClipboardItem,
   8    Context, DragMoveEvent, Element, Entity, Focusable, GlobalElementId, Hsla, InspectorElementId,
   9    IntoElement, LayoutId, Length, Modifiers, MouseButton, ParentElement, Pixels,
  10    StatefulInteractiveElement, Styled, TextStyleRefinement, Window, div, linear_color_stop,
  11    linear_gradient, point, px, size,
  12};
  13use multi_buffer::{Anchor, ExcerptId, ExcerptInfo};
  14use project::Entry;
  15use settings::Settings;
  16use text::BufferId;
  17use theme::ActiveTheme;
  18use ui::scrollbars::ShowScrollbar;
  19use ui::{
  20    Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName, Indicator, KeyBinding, Label,
  21    Tooltip, h_flex, prelude::*, right_click_menu, text_for_keystroke, v_flex,
  22};
  23use workspace::{ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel};
  24
  25use crate::{
  26    DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, JumpData,
  27    MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, RowExt, StickyHeaderExcerpt, ToggleFold,
  28    ToggleFoldAll,
  29    display_map::Block,
  30    element::{EditorElement, SplitSide},
  31    scroll::ScrollOffset,
  32    split::SplittableEditor,
  33};
  34
  35const RESIZE_HANDLE_WIDTH: f32 = 8.0;
  36
  37#[derive(Debug, Clone)]
  38struct DraggedSplitHandle;
  39
  40pub struct SplitEditorState {
  41    left_ratio: f32,
  42    visible_left_ratio: f32,
  43    cached_width: Pixels,
  44}
  45
  46impl SplitEditorState {
  47    pub fn new(_cx: &mut App) -> Self {
  48        Self {
  49            left_ratio: 0.5,
  50            visible_left_ratio: 0.5,
  51            cached_width: px(0.),
  52        }
  53    }
  54
  55    #[allow(clippy::misnamed_getters)]
  56    pub fn left_ratio(&self) -> f32 {
  57        self.visible_left_ratio
  58    }
  59
  60    pub fn right_ratio(&self) -> f32 {
  61        1.0 - self.visible_left_ratio
  62    }
  63
  64    fn on_drag_move(
  65        &mut self,
  66        drag_event: &DragMoveEvent<DraggedSplitHandle>,
  67        _window: &mut Window,
  68        _cx: &mut Context<Self>,
  69    ) {
  70        let drag_position = drag_event.event.position;
  71        let bounds = drag_event.bounds;
  72        let bounds_width = bounds.right() - bounds.left();
  73
  74        if bounds_width > px(0.) {
  75            self.cached_width = bounds_width;
  76        }
  77
  78        let min_ratio = 0.1;
  79        let max_ratio = 0.9;
  80
  81        let new_ratio = (drag_position.x - bounds.left()) / bounds_width;
  82        self.visible_left_ratio = new_ratio.clamp(min_ratio, max_ratio);
  83    }
  84
  85    fn commit_ratio(&mut self) {
  86        self.left_ratio = self.visible_left_ratio;
  87    }
  88
  89    fn on_double_click(&mut self) {
  90        self.left_ratio = 0.5;
  91        self.visible_left_ratio = 0.5;
  92    }
  93}
  94
  95#[derive(IntoElement)]
  96pub struct SplitEditorView {
  97    splittable_editor: Entity<SplittableEditor>,
  98    style: EditorStyle,
  99    split_state: Entity<SplitEditorState>,
 100}
 101
 102impl SplitEditorView {
 103    pub fn new(
 104        splittable_editor: Entity<SplittableEditor>,
 105        style: EditorStyle,
 106        split_state: Entity<SplitEditorState>,
 107    ) -> Self {
 108        Self {
 109            splittable_editor,
 110            style,
 111            split_state,
 112        }
 113    }
 114}
 115
 116fn render_resize_handle(
 117    state: &Entity<SplitEditorState>,
 118    separator_color: Hsla,
 119    _window: &mut Window,
 120    _cx: &mut App,
 121) -> AnyElement {
 122    let state_for_click = state.clone();
 123
 124    div()
 125        .id("split-resize-container")
 126        .relative()
 127        .h_full()
 128        .flex_shrink_0()
 129        .w(px(1.))
 130        .bg(separator_color)
 131        .child(
 132            div()
 133                .id("split-resize-handle")
 134                .absolute()
 135                .left(px(-RESIZE_HANDLE_WIDTH / 2.0))
 136                .w(px(RESIZE_HANDLE_WIDTH))
 137                .h_full()
 138                .cursor_col_resize()
 139                .block_mouse_except_scroll()
 140                .on_click(move |event, _, cx| {
 141                    if event.click_count() >= 2 {
 142                        state_for_click.update(cx, |state, _| {
 143                            state.on_double_click();
 144                        });
 145                    }
 146                    cx.stop_propagation();
 147                })
 148                .on_drag(DraggedSplitHandle, |_, _, _, cx| cx.new(|_| gpui::Empty)),
 149        )
 150        .into_any_element()
 151}
 152
 153impl RenderOnce for SplitEditorView {
 154    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 155        let splittable_editor = self.splittable_editor.read(cx);
 156
 157        assert!(
 158            splittable_editor.secondary_editor().is_some(),
 159            "`SplitEditorView` requires `SplittableEditor` to be in split mode"
 160        );
 161
 162        let lhs_editor = splittable_editor.secondary_editor().unwrap().clone();
 163        let rhs_editor = splittable_editor.primary_editor().clone();
 164
 165        let mut lhs = EditorElement::new(&lhs_editor, self.style.clone());
 166        let mut rhs = EditorElement::new(&rhs_editor, self.style.clone());
 167
 168        lhs.set_split_side(SplitSide::Left);
 169        rhs.set_split_side(SplitSide::Right);
 170
 171        let left_ratio = self.split_state.read(cx).left_ratio();
 172        let right_ratio = self.split_state.read(cx).right_ratio();
 173
 174        let separator_color = cx.theme().colors().border_variant;
 175
 176        let resize_handle = render_resize_handle(&self.split_state, separator_color, window, cx);
 177
 178        let state_for_drag = self.split_state.downgrade();
 179        let state_for_drop = self.split_state.downgrade();
 180
 181        let buffer_headers = SplitBufferHeadersElement::new(rhs_editor, self.style.clone());
 182
 183        div()
 184            .id("split-editor-view-container")
 185            .size_full()
 186            .relative()
 187            .child(
 188                h_flex()
 189                    .id("split-editor-view")
 190                    .size_full()
 191                    .on_drag_move::<DraggedSplitHandle>(move |event, window, cx| {
 192                        state_for_drag
 193                            .update(cx, |state, cx| {
 194                                state.on_drag_move(event, window, cx);
 195                            })
 196                            .ok();
 197                    })
 198                    .on_drop::<DraggedSplitHandle>(move |_, _, cx| {
 199                        state_for_drop
 200                            .update(cx, |state, _| {
 201                                state.commit_ratio();
 202                            })
 203                            .ok();
 204                    })
 205                    .child(
 206                        div()
 207                            .id("split-editor-left")
 208                            .flex_shrink()
 209                            .min_w_0()
 210                            .h_full()
 211                            .flex_basis(DefiniteLength::Fraction(left_ratio))
 212                            .overflow_hidden()
 213                            .child(lhs),
 214                    )
 215                    .child(resize_handle)
 216                    .child(
 217                        div()
 218                            .id("split-editor-right")
 219                            .flex_shrink()
 220                            .min_w_0()
 221                            .h_full()
 222                            .flex_basis(DefiniteLength::Fraction(right_ratio))
 223                            .overflow_hidden()
 224                            .child(rhs),
 225                    ),
 226            )
 227            .child(buffer_headers)
 228    }
 229}
 230
 231struct SplitBufferHeadersElement {
 232    editor: Entity<Editor>,
 233    style: EditorStyle,
 234}
 235
 236impl SplitBufferHeadersElement {
 237    fn new(editor: Entity<Editor>, style: EditorStyle) -> Self {
 238        Self { editor, style }
 239    }
 240}
 241
 242struct BufferHeaderLayout {
 243    element: AnyElement,
 244}
 245
 246struct SplitBufferHeadersPrepaintState {
 247    sticky_header: Option<AnyElement>,
 248    non_sticky_headers: Vec<BufferHeaderLayout>,
 249}
 250
 251impl IntoElement for SplitBufferHeadersElement {
 252    type Element = Self;
 253
 254    fn into_element(self) -> Self::Element {
 255        self
 256    }
 257}
 258
 259impl Element for SplitBufferHeadersElement {
 260    type RequestLayoutState = ();
 261    type PrepaintState = SplitBufferHeadersPrepaintState;
 262
 263    fn id(&self) -> Option<gpui::ElementId> {
 264        Some("split-buffer-headers".into())
 265    }
 266
 267    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
 268        None
 269    }
 270
 271    fn request_layout(
 272        &mut self,
 273        _id: Option<&GlobalElementId>,
 274        _inspector_id: Option<&InspectorElementId>,
 275        window: &mut Window,
 276        _cx: &mut App,
 277    ) -> (LayoutId, Self::RequestLayoutState) {
 278        let mut style = gpui::Style::default();
 279        style.position = gpui::Position::Absolute;
 280        style.inset.top = DefiniteLength::Fraction(0.0).into();
 281        style.inset.left = DefiniteLength::Fraction(0.0).into();
 282        style.size.width = Length::Definite(DefiniteLength::Fraction(1.0));
 283        style.size.height = Length::Definite(DefiniteLength::Fraction(1.0));
 284        let layout_id = window.request_layout(style, [], _cx);
 285        (layout_id, ())
 286    }
 287
 288    fn prepaint(
 289        &mut self,
 290        _id: Option<&GlobalElementId>,
 291        _inspector_id: Option<&InspectorElementId>,
 292        bounds: Bounds<Pixels>,
 293        _request_layout: &mut Self::RequestLayoutState,
 294        window: &mut Window,
 295        cx: &mut App,
 296    ) -> Self::PrepaintState {
 297        if bounds.size.width <= px(0.) || bounds.size.height <= px(0.) {
 298            return SplitBufferHeadersPrepaintState {
 299                sticky_header: None,
 300                non_sticky_headers: Vec::new(),
 301            };
 302        }
 303
 304        let rem_size = self.rem_size();
 305        let text_style = TextStyleRefinement {
 306            font_size: Some(self.style.text.font_size),
 307            line_height: Some(self.style.text.line_height),
 308            ..Default::default()
 309        };
 310
 311        window.with_rem_size(rem_size, |window| {
 312            window.with_text_style(Some(text_style), |window| {
 313                Self::prepaint_inner(self, bounds, window, cx)
 314            })
 315        })
 316    }
 317
 318    fn paint(
 319        &mut self,
 320        _id: Option<&GlobalElementId>,
 321        _inspector_id: Option<&InspectorElementId>,
 322        _bounds: Bounds<Pixels>,
 323        _request_layout: &mut Self::RequestLayoutState,
 324        prepaint: &mut Self::PrepaintState,
 325        window: &mut Window,
 326        cx: &mut App,
 327    ) {
 328        let rem_size = self.rem_size();
 329        let text_style = TextStyleRefinement {
 330            font_size: Some(self.style.text.font_size),
 331            line_height: Some(self.style.text.line_height),
 332            ..Default::default()
 333        };
 334
 335        window.with_rem_size(rem_size, |window| {
 336            window.with_text_style(Some(text_style), |window| {
 337                for header_layout in &mut prepaint.non_sticky_headers {
 338                    header_layout.element.paint(window, cx);
 339                }
 340
 341                if let Some(mut sticky_header) = prepaint.sticky_header.take() {
 342                    sticky_header.paint(window, cx);
 343                }
 344            });
 345        });
 346    }
 347}
 348
 349impl SplitBufferHeadersElement {
 350    fn rem_size(&self) -> Option<Pixels> {
 351        match self.style.text.font_size {
 352            AbsoluteLength::Pixels(pixels) => {
 353                let rem_size_scale = {
 354                    let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
 355                    let default_font_size_delta = 1. - default_font_size_scale;
 356                    1. + default_font_size_delta
 357                };
 358
 359                Some(pixels * rem_size_scale)
 360            }
 361            AbsoluteLength::Rems(rems) => Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into())),
 362        }
 363    }
 364
 365    fn prepaint_inner(
 366        &mut self,
 367        bounds: Bounds<Pixels>,
 368        window: &mut Window,
 369        cx: &mut App,
 370    ) -> SplitBufferHeadersPrepaintState {
 371        let line_height = window.line_height();
 372
 373        let snapshot = self
 374            .editor
 375            .update(cx, |editor, cx| editor.snapshot(window, cx));
 376        let scroll_position = snapshot.scroll_position();
 377
 378        // Compute right margin to avoid overlapping the scrollbar
 379        let settings = EditorSettings::get_global(cx);
 380        let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never;
 381        let vertical_scrollbar_width = (scrollbars_shown
 382            && settings.scrollbar.axes.vertical
 383            && self.editor.read(cx).show_scrollbars.vertical)
 384            .then_some(EditorElement::SCROLLBAR_WIDTH)
 385            .unwrap_or_default();
 386        let available_width = bounds.size.width - vertical_scrollbar_width;
 387
 388        let visible_height_in_lines = bounds.size.height / line_height;
 389        let max_row = snapshot.max_point().row();
 390        let start_row = cmp::min(DisplayRow(scroll_position.y.floor() as u32), max_row);
 391        let end_row = cmp::min(
 392            (scroll_position.y + visible_height_in_lines as f64).ceil() as u32,
 393            max_row.next_row().0,
 394        );
 395        let end_row = DisplayRow(end_row);
 396
 397        let (selected_buffer_ids, latest_selection_anchors) =
 398            self.compute_selection_info(&snapshot, cx);
 399
 400        let sticky_header = if snapshot.buffer_snapshot().show_headers() {
 401            snapshot
 402                .sticky_header_excerpt(scroll_position.y)
 403                .map(|sticky_excerpt| {
 404                    self.build_sticky_header(
 405                        sticky_excerpt,
 406                        &snapshot,
 407                        scroll_position,
 408                        bounds,
 409                        available_width,
 410                        line_height,
 411                        &selected_buffer_ids,
 412                        &latest_selection_anchors,
 413                        start_row,
 414                        end_row,
 415                        window,
 416                        cx,
 417                    )
 418                })
 419        } else {
 420            None
 421        };
 422
 423        let sticky_header_excerpt_id = snapshot
 424            .sticky_header_excerpt(scroll_position.y)
 425            .map(|e| e.excerpt.id);
 426
 427        let non_sticky_headers = self.build_non_sticky_headers(
 428            &snapshot,
 429            scroll_position,
 430            bounds,
 431            available_width,
 432            line_height,
 433            start_row,
 434            end_row,
 435            &selected_buffer_ids,
 436            &latest_selection_anchors,
 437            sticky_header_excerpt_id,
 438            window,
 439            cx,
 440        );
 441
 442        SplitBufferHeadersPrepaintState {
 443            sticky_header,
 444            non_sticky_headers,
 445        }
 446    }
 447
 448    fn compute_selection_info(
 449        &self,
 450        snapshot: &EditorSnapshot,
 451        cx: &App,
 452    ) -> (HashSet<BufferId>, HashMap<BufferId, Anchor>) {
 453        let editor = self.editor.read(cx);
 454        let all_selections = editor
 455            .selections
 456            .all::<crate::Point>(&snapshot.display_snapshot);
 457        let all_anchor_selections = editor.selections.all_anchors(&snapshot.display_snapshot);
 458
 459        let mut selected_buffer_ids = HashSet::default();
 460        for selection in &all_selections {
 461            for buffer_id in snapshot
 462                .buffer_snapshot()
 463                .buffer_ids_for_range(selection.range())
 464            {
 465                selected_buffer_ids.insert(buffer_id);
 466            }
 467        }
 468
 469        let mut anchors_by_buffer: HashMap<BufferId, (usize, Anchor)> = HashMap::default();
 470        for selection in all_anchor_selections.iter() {
 471            let head = selection.head();
 472            if let Some(buffer_id) = head.text_anchor.buffer_id {
 473                anchors_by_buffer
 474                    .entry(buffer_id)
 475                    .and_modify(|(latest_id, latest_anchor)| {
 476                        if selection.id > *latest_id {
 477                            *latest_id = selection.id;
 478                            *latest_anchor = head;
 479                        }
 480                    })
 481                    .or_insert((selection.id, head));
 482            }
 483        }
 484        let latest_selection_anchors = anchors_by_buffer
 485            .into_iter()
 486            .map(|(buffer_id, (_, anchor))| (buffer_id, anchor))
 487            .collect();
 488
 489        (selected_buffer_ids, latest_selection_anchors)
 490    }
 491
 492    fn build_sticky_header(
 493        &self,
 494        StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>,
 495        snapshot: &EditorSnapshot,
 496        scroll_position: gpui::Point<ScrollOffset>,
 497        bounds: Bounds<Pixels>,
 498        available_width: Pixels,
 499        line_height: Pixels,
 500        selected_buffer_ids: &HashSet<BufferId>,
 501        latest_selection_anchors: &HashMap<BufferId, Anchor>,
 502        start_row: DisplayRow,
 503        end_row: DisplayRow,
 504        window: &mut Window,
 505        cx: &mut App,
 506    ) -> AnyElement {
 507        let jump_data = header_jump_data(
 508            snapshot,
 509            DisplayRow(scroll_position.y as u32),
 510            FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
 511            excerpt,
 512            latest_selection_anchors,
 513        );
 514
 515        let editor_bg_color = cx.theme().colors().editor_background;
 516        let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
 517
 518        let mut header = v_flex()
 519            .id("sticky-buffer-header")
 520            .w(available_width)
 521            .relative()
 522            .child(
 523                div()
 524                    .w(available_width)
 525                    .h(FILE_HEADER_HEIGHT as f32 * line_height)
 526                    .bg(linear_gradient(
 527                        0.,
 528                        linear_color_stop(editor_bg_color.opacity(0.), 0.),
 529                        linear_color_stop(editor_bg_color, 0.6),
 530                    ))
 531                    .absolute()
 532                    .top_0(),
 533            )
 534            .child(
 535                self.render_buffer_header(excerpt, false, selected, true, jump_data, window, cx)
 536                    .into_any_element(),
 537            )
 538            .into_any_element();
 539
 540        let mut origin = bounds.origin;
 541
 542        for (block_row, block) in snapshot.blocks_in_range(start_row..end_row) {
 543            if !block.is_buffer_header() {
 544                continue;
 545            }
 546
 547            if block_row.0 <= scroll_position.y as u32 {
 548                continue;
 549            }
 550
 551            let max_row = block_row.0.saturating_sub(FILE_HEADER_HEIGHT);
 552            let offset = scroll_position.y - max_row as f64;
 553
 554            if offset > 0.0 {
 555                origin.y -= Pixels::from(offset * f64::from(line_height));
 556            }
 557            break;
 558        }
 559
 560        let available_size = size(
 561            AvailableSpace::Definite(available_width),
 562            AvailableSpace::MinContent,
 563        );
 564
 565        header.prepaint_as_root(origin, available_size, window, cx);
 566
 567        header
 568    }
 569
 570    fn build_non_sticky_headers(
 571        &self,
 572        snapshot: &EditorSnapshot,
 573        scroll_position: gpui::Point<ScrollOffset>,
 574        bounds: Bounds<Pixels>,
 575        available_width: Pixels,
 576        line_height: Pixels,
 577        start_row: DisplayRow,
 578        end_row: DisplayRow,
 579        selected_buffer_ids: &HashSet<BufferId>,
 580        latest_selection_anchors: &HashMap<BufferId, Anchor>,
 581        sticky_header_excerpt_id: Option<ExcerptId>,
 582        window: &mut Window,
 583        cx: &mut App,
 584    ) -> Vec<BufferHeaderLayout> {
 585        let mut headers = Vec::new();
 586
 587        for (block_row, block) in snapshot.blocks_in_range(start_row..end_row) {
 588            let (excerpt, is_folded) = match block {
 589                Block::BufferHeader { excerpt, .. } => {
 590                    if sticky_header_excerpt_id == Some(excerpt.id) {
 591                        continue;
 592                    }
 593                    (excerpt, false)
 594                }
 595                Block::FoldedBuffer { first_excerpt, .. } => (first_excerpt, true),
 596                // ExcerptBoundary is just a separator line, not a buffer header
 597                Block::ExcerptBoundary { .. } | Block::Custom(_) | Block::Spacer { .. } => continue,
 598            };
 599
 600            let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
 601            let jump_data = header_jump_data(
 602                snapshot,
 603                block_row,
 604                block.height(),
 605                excerpt,
 606                latest_selection_anchors,
 607            );
 608
 609            let mut header = self
 610                .render_buffer_header(excerpt, is_folded, selected, false, jump_data, window, cx)
 611                .into_any_element();
 612
 613            let y_offset = (block_row.0 as f64 - scroll_position.y) * f64::from(line_height);
 614            let origin = point(bounds.origin.x, bounds.origin.y + Pixels::from(y_offset));
 615
 616            let available_size = size(
 617                AvailableSpace::Definite(available_width),
 618                AvailableSpace::MinContent,
 619            );
 620
 621            header.prepaint_as_root(origin, available_size, window, cx);
 622
 623            headers.push(BufferHeaderLayout { element: header });
 624        }
 625
 626        headers
 627    }
 628
 629    fn render_buffer_header(
 630        &self,
 631        for_excerpt: &ExcerptInfo,
 632        is_folded: bool,
 633        is_selected: bool,
 634        is_sticky: bool,
 635        jump_data: JumpData,
 636        window: &mut Window,
 637        cx: &mut App,
 638    ) -> impl IntoElement {
 639        let editor = self.editor.read(cx);
 640        let multi_buffer = editor.buffer.read(cx);
 641        let is_read_only = self.editor.read(cx).read_only(cx);
 642
 643        let file_status = multi_buffer
 644            .all_diff_hunks_expanded()
 645            .then(|| editor.status_for_buffer_id(for_excerpt.buffer_id, cx))
 646            .flatten();
 647        let indicator = multi_buffer
 648            .buffer(for_excerpt.buffer_id)
 649            .and_then(|buffer| {
 650                let buffer = buffer.read(cx);
 651                let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) {
 652                    (true, _) => Some(Color::Warning),
 653                    (_, true) => Some(Color::Accent),
 654                    (false, false) => None,
 655                };
 656                indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color))
 657            });
 658
 659        let include_root = editor
 660            .project
 661            .as_ref()
 662            .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
 663            .unwrap_or_default();
 664        let file = for_excerpt.buffer.file();
 665        let can_open_excerpts = file.is_none_or(|file| file.can_open());
 666        let path_style = file.map(|file| file.path_style(cx));
 667        let relative_path = for_excerpt.buffer.resolve_file_path(include_root, cx);
 668        let (parent_path, filename) = if let Some(path) = &relative_path {
 669            if let Some(path_style) = path_style {
 670                let (dir, file_name) = path_style.split(path);
 671                (dir.map(|dir| dir.to_owned()), Some(file_name.to_owned()))
 672            } else {
 673                (None, Some(path.clone()))
 674            }
 675        } else {
 676            (None, None)
 677        };
 678        let focus_handle = self.editor.read(cx).focus_handle(cx);
 679        let colors = cx.theme().colors();
 680
 681        let header = div()
 682            .p_1()
 683            .w_full()
 684            .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
 685            .child(
 686                h_flex()
 687                    .size_full()
 688                    .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
 689                    .pl_1()
 690                    .pr_2()
 691                    .rounded_sm()
 692                    .gap_1p5()
 693                    .when(is_sticky, |el| el.shadow_md())
 694                    .border_1()
 695                    .map(|border| {
 696                        let border_color = if !is_sticky
 697                            && is_selected
 698                            && is_folded
 699                            && focus_handle.contains_focused(window, cx)
 700                        {
 701                            colors.border_focused
 702                        } else {
 703                            colors.border
 704                        };
 705                        border.border_color(border_color)
 706                    })
 707                    .bg(colors.editor_subheader_background)
 708                    .hover(|style| style.bg(colors.element_hover))
 709                    .map(|header| {
 710                        let editor = self.editor.clone();
 711                        let buffer_id = for_excerpt.buffer_id;
 712                        let toggle_chevron_icon =
 713                            FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
 714                        let button_size = rems_from_px(28.);
 715
 716                        header.child(
 717                            div()
 718                                .hover(|style| style.bg(colors.element_selected))
 719                                .rounded_xs()
 720                                .child(
 721                                    ButtonLike::new("toggle-buffer-fold")
 722                                        .style(ButtonStyle::Transparent)
 723                                        .height(button_size.into())
 724                                        .width(button_size)
 725                                        .children(toggle_chevron_icon)
 726                                        .tooltip({
 727                                            let focus_handle = focus_handle.clone();
 728                                            let is_folded_for_tooltip = is_folded;
 729                                            move |_window, cx| {
 730                                                Tooltip::with_meta_in(
 731                                                    if is_folded_for_tooltip {
 732                                                        "Unfold Excerpt"
 733                                                    } else {
 734                                                        "Fold Excerpt"
 735                                                    },
 736                                                    Some(&ToggleFold),
 737                                                    format!(
 738                                                        "{} to toggle all",
 739                                                        text_for_keystroke(
 740                                                            &Modifiers::alt(),
 741                                                            "click",
 742                                                            cx
 743                                                        )
 744                                                    ),
 745                                                    &focus_handle,
 746                                                    cx,
 747                                                )
 748                                            }
 749                                        })
 750                                        .on_click(move |event, window, cx| {
 751                                            if event.modifiers().alt {
 752                                                editor.update(cx, |editor, cx| {
 753                                                    editor.toggle_fold_all(
 754                                                        &ToggleFoldAll,
 755                                                        window,
 756                                                        cx,
 757                                                    );
 758                                                });
 759                                            } else {
 760                                                if is_folded {
 761                                                    editor.update(cx, |editor, cx| {
 762                                                        editor.unfold_buffer(buffer_id, cx);
 763                                                    });
 764                                                } else {
 765                                                    editor.update(cx, |editor, cx| {
 766                                                        editor.fold_buffer(buffer_id, cx);
 767                                                    });
 768                                                }
 769                                            }
 770                                        }),
 771                                ),
 772                        )
 773                    })
 774                    .children(
 775                        editor
 776                            .addons
 777                            .values()
 778                            .filter_map(|addon| {
 779                                addon.render_buffer_header_controls(for_excerpt, window, cx)
 780                            })
 781                            .take(1),
 782                    )
 783                    .when(!is_read_only, |this| {
 784                        this.child(
 785                            h_flex()
 786                                .size_3()
 787                                .justify_center()
 788                                .flex_shrink_0()
 789                                .children(indicator),
 790                        )
 791                    })
 792                    .child(
 793                        h_flex()
 794                            .cursor_pointer()
 795                            .id("path_header_block")
 796                            .min_w_0()
 797                            .size_full()
 798                            .justify_between()
 799                            .overflow_hidden()
 800                            .child(h_flex().min_w_0().flex_1().gap_0p5().map(|path_header| {
 801                                let filename = filename
 802                                    .map(SharedString::from)
 803                                    .unwrap_or_else(|| "untitled".into());
 804
 805                                path_header
 806                                    .when(ItemSettings::get_global(cx).file_icons, |el| {
 807                                        let path = path::Path::new(filename.as_str());
 808                                        let icon =
 809                                            FileIcons::get_icon(path, cx).unwrap_or_default();
 810
 811                                        el.child(Icon::from_path(icon).color(Color::Muted))
 812                                    })
 813                                    .child(
 814                                        ButtonLike::new("filename-button")
 815                                            .child(
 816                                                Label::new(filename)
 817                                                    .single_line()
 818                                                    .color(file_status_label_color(file_status))
 819                                                    .when(
 820                                                        file_status.is_some_and(|s| s.is_deleted()),
 821                                                        |label| label.strikethrough(),
 822                                                    ),
 823                                            )
 824                                            .on_click(window.listener_for(&self.editor, {
 825                                                let jump_data = jump_data.clone();
 826                                                move |editor, e: &ClickEvent, window, cx| {
 827                                                    editor.open_excerpts_common(
 828                                                        Some(jump_data.clone()),
 829                                                        e.modifiers().secondary(),
 830                                                        window,
 831                                                        cx,
 832                                                    );
 833                                                }
 834                                            })),
 835                                    )
 836                                    .when(!for_excerpt.buffer.capability.editable(), |el| {
 837                                        el.child(Icon::new(IconName::FileLock).color(Color::Muted))
 838                                    })
 839                                    .when_some(parent_path, |then, path| {
 840                                        then.child(Label::new(path).truncate().color(
 841                                            if file_status.is_some_and(FileStatus::is_deleted) {
 842                                                Color::Custom(colors.text_disabled)
 843                                            } else {
 844                                                Color::Custom(colors.text_muted)
 845                                            },
 846                                        ))
 847                                    })
 848                            }))
 849                            .when(
 850                                can_open_excerpts && is_selected && relative_path.is_some(),
 851                                |el| {
 852                                    el.child(
 853                                        Button::new("open-file-button", "Open File")
 854                                            .style(ButtonStyle::OutlinedGhost)
 855                                            .key_binding(KeyBinding::for_action_in(
 856                                                &OpenExcerpts,
 857                                                &focus_handle,
 858                                                cx,
 859                                            ))
 860                                            .on_click(window.listener_for(&self.editor, {
 861                                                let jump_data = jump_data.clone();
 862                                                move |editor, e: &ClickEvent, window, cx| {
 863                                                    editor.open_excerpts_common(
 864                                                        Some(jump_data.clone()),
 865                                                        e.modifiers().secondary(),
 866                                                        window,
 867                                                        cx,
 868                                                    );
 869                                                }
 870                                            })),
 871                                    )
 872                                },
 873                            )
 874                            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
 875                            .on_click(window.listener_for(&self.editor, {
 876                                let buffer_id = for_excerpt.buffer_id;
 877                                move |editor, e: &ClickEvent, window, cx| {
 878                                    if e.modifiers().alt {
 879                                        editor.open_excerpts_common(
 880                                            Some(jump_data.clone()),
 881                                            e.modifiers().secondary(),
 882                                            window,
 883                                            cx,
 884                                        );
 885                                        return;
 886                                    }
 887
 888                                    if is_folded {
 889                                        editor.unfold_buffer(buffer_id, cx);
 890                                    } else {
 891                                        editor.fold_buffer(buffer_id, cx);
 892                                    }
 893                                }
 894                            })),
 895                    ),
 896            );
 897
 898        let file = for_excerpt.buffer.file().cloned();
 899        let editor = self.editor.clone();
 900
 901        right_click_menu("buffer-header-context-menu")
 902            .trigger(move |_, _, _| header)
 903            .menu(move |window, cx| {
 904                let menu_context = focus_handle.clone();
 905                let editor = editor.clone();
 906                let file = file.clone();
 907                ContextMenu::build(window, cx, move |mut menu, window, cx| {
 908                    if let Some(file) = file
 909                        && let Some(project) = editor.read(cx).project()
 910                        && let Some(worktree) =
 911                            project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
 912                    {
 913                        let path_style = file.path_style(cx);
 914                        let worktree = worktree.read(cx);
 915                        let relative_path = file.path();
 916                        let entry_for_path = worktree.entry_for_path(relative_path);
 917                        let abs_path = entry_for_path.map(|e| {
 918                            e.canonical_path.as_deref().map_or_else(
 919                                || worktree.absolutize(relative_path),
 920                                Path::to_path_buf,
 921                            )
 922                        });
 923                        let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
 924
 925                        let parent_abs_path = abs_path
 926                            .as_ref()
 927                            .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
 928                        let relative_path = has_relative_path
 929                            .then_some(relative_path)
 930                            .map(ToOwned::to_owned);
 931
 932                        let visible_in_project_panel =
 933                            relative_path.is_some() && worktree.is_visible();
 934                        let reveal_in_project_panel = entry_for_path
 935                            .filter(|_| visible_in_project_panel)
 936                            .map(|entry| entry.id);
 937                        menu = menu
 938                            .when_some(abs_path, |menu, abs_path| {
 939                                menu.entry(
 940                                    "Copy Path",
 941                                    Some(Box::new(zed_actions::workspace::CopyPath)),
 942                                    window.handler_for(&editor, move |_, _, cx| {
 943                                        cx.write_to_clipboard(ClipboardItem::new_string(
 944                                            abs_path.to_string_lossy().into_owned(),
 945                                        ));
 946                                    }),
 947                                )
 948                            })
 949                            .when_some(relative_path, |menu, relative_path| {
 950                                menu.entry(
 951                                    "Copy Relative Path",
 952                                    Some(Box::new(zed_actions::workspace::CopyRelativePath)),
 953                                    window.handler_for(&editor, move |_, _, cx| {
 954                                        cx.write_to_clipboard(ClipboardItem::new_string(
 955                                            relative_path.display(path_style).to_string(),
 956                                        ));
 957                                    }),
 958                                )
 959                            })
 960                            .when(
 961                                reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
 962                                |menu| menu.separator(),
 963                            )
 964                            .when_some(reveal_in_project_panel, |menu, entry_id| {
 965                                menu.entry(
 966                                    "Reveal In Project Panel",
 967                                    Some(Box::new(RevealInProjectPanel::default())),
 968                                    window.handler_for(&editor, move |editor, _, cx| {
 969                                        if let Some(project) = &mut editor.project {
 970                                            project.update(cx, |_, cx| {
 971                                                cx.emit(project::Event::RevealInProjectPanel(
 972                                                    entry_id,
 973                                                ))
 974                                            });
 975                                        }
 976                                    }),
 977                                )
 978                            })
 979                            .when_some(parent_abs_path, |menu, parent_abs_path| {
 980                                menu.entry(
 981                                    "Open in Terminal",
 982                                    Some(Box::new(OpenInTerminal)),
 983                                    window.handler_for(&editor, move |_, window, cx| {
 984                                        window.dispatch_action(
 985                                            OpenTerminal {
 986                                                working_directory: parent_abs_path.clone(),
 987                                                local: false,
 988                                            }
 989                                            .boxed_clone(),
 990                                            cx,
 991                                        );
 992                                    }),
 993                                )
 994                            });
 995                    }
 996
 997                    menu.context(menu_context)
 998                })
 999            })
1000    }
1001}
1002
1003fn header_jump_data(
1004    editor_snapshot: &EditorSnapshot,
1005    block_row_start: DisplayRow,
1006    height: u32,
1007    first_excerpt: &ExcerptInfo,
1008    latest_selection_anchors: &HashMap<BufferId, Anchor>,
1009) -> JumpData {
1010    let jump_target = if let Some(anchor) = latest_selection_anchors.get(&first_excerpt.buffer_id)
1011        && let Some(range) = editor_snapshot.context_range_for_excerpt(anchor.excerpt_id)
1012        && let Some(buffer) = editor_snapshot
1013            .buffer_snapshot()
1014            .buffer_for_excerpt(anchor.excerpt_id)
1015    {
1016        JumpTargetInExcerptInput {
1017            id: anchor.excerpt_id,
1018            buffer,
1019            excerpt_start_anchor: range.start,
1020            jump_anchor: anchor.text_anchor,
1021        }
1022    } else {
1023        JumpTargetInExcerptInput {
1024            id: first_excerpt.id,
1025            buffer: &first_excerpt.buffer,
1026            excerpt_start_anchor: first_excerpt.range.context.start,
1027            jump_anchor: first_excerpt.range.primary.start,
1028        }
1029    };
1030    header_jump_data_inner(editor_snapshot, block_row_start, height, &jump_target)
1031}
1032
1033struct JumpTargetInExcerptInput<'a> {
1034    id: ExcerptId,
1035    buffer: &'a language::BufferSnapshot,
1036    excerpt_start_anchor: text::Anchor,
1037    jump_anchor: text::Anchor,
1038}
1039
1040fn header_jump_data_inner(
1041    snapshot: &EditorSnapshot,
1042    block_row_start: DisplayRow,
1043    height: u32,
1044    for_excerpt: &JumpTargetInExcerptInput,
1045) -> JumpData {
1046    let buffer = &for_excerpt.buffer;
1047    let jump_position = language::ToPoint::to_point(&for_excerpt.jump_anchor, buffer);
1048    let excerpt_start = for_excerpt.excerpt_start_anchor;
1049    let rows_from_excerpt_start = if for_excerpt.jump_anchor == excerpt_start {
1050        0
1051    } else {
1052        let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
1053        jump_position.row.saturating_sub(excerpt_start_point.row)
1054    };
1055
1056    let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start)
1057        .saturating_sub(
1058            snapshot
1059                .scroll_anchor
1060                .scroll_position(&snapshot.display_snapshot)
1061                .y as u32,
1062        );
1063
1064    JumpData::MultiBufferPoint {
1065        excerpt_id: for_excerpt.id,
1066        anchor: for_excerpt.jump_anchor,
1067        position: jump_position,
1068        line_offset_from_top,
1069    }
1070}
1071
1072fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
1073    file_status.map_or(Color::Default, |status| {
1074        if status.is_conflicted() {
1075            Color::Conflict
1076        } else if status.is_modified() {
1077            Color::Modified
1078        } else if status.is_deleted() {
1079            Color::Disabled
1080        } else if status.is_created() {
1081            Color::Created
1082        } else {
1083            Color::Default
1084        }
1085    })
1086}