Skip to repository content1108 lines · 46.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:36:18.469Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
header.rs
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