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}