1use std::cmp;
2
3use collections::{HashMap, HashSet};
4use gpui::{
5 AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, Context, DragMoveEvent, Element,
6 Entity, GlobalElementId, Hsla, InspectorElementId, IntoElement, LayoutId, Length,
7 ParentElement, Pixels, StatefulInteractiveElement, Styled, TextStyleRefinement, Window, div,
8 linear_color_stop, linear_gradient, point, px, size,
9};
10use multi_buffer::{Anchor, ExcerptId};
11use settings::Settings;
12use smallvec::smallvec;
13use text::BufferId;
14use theme::ActiveTheme;
15use ui::scrollbars::ShowScrollbar;
16use ui::{h_flex, prelude::*, v_flex};
17
18use gpui::ContentMask;
19
20use crate::{
21 DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
22 MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, RowExt, StickyHeaderExcerpt,
23 display_map::Block,
24 element::{EditorElement, SplitSide, header_jump_data, render_buffer_header},
25 scroll::ScrollOffset,
26 split::SplittableEditor,
27};
28
29const RESIZE_HANDLE_WIDTH: f32 = 8.0;
30
31#[derive(Debug, Clone)]
32struct DraggedSplitHandle;
33
34pub struct SplitEditorState {
35 left_ratio: f32,
36 visible_left_ratio: f32,
37 cached_width: Pixels,
38}
39
40impl SplitEditorState {
41 pub fn new(_cx: &mut App) -> Self {
42 Self {
43 left_ratio: 0.5,
44 visible_left_ratio: 0.5,
45 cached_width: px(0.),
46 }
47 }
48
49 #[allow(clippy::misnamed_getters)]
50 pub fn left_ratio(&self) -> f32 {
51 self.visible_left_ratio
52 }
53
54 pub fn right_ratio(&self) -> f32 {
55 1.0 - self.visible_left_ratio
56 }
57
58 fn on_drag_move(
59 &mut self,
60 drag_event: &DragMoveEvent<DraggedSplitHandle>,
61 _window: &mut Window,
62 _cx: &mut Context<Self>,
63 ) {
64 let drag_position = drag_event.event.position;
65 let bounds = drag_event.bounds;
66 let bounds_width = bounds.right() - bounds.left();
67
68 if bounds_width > px(0.) {
69 self.cached_width = bounds_width;
70 }
71
72 let min_ratio = 0.1;
73 let max_ratio = 0.9;
74
75 let new_ratio = (drag_position.x - bounds.left()) / bounds_width;
76 self.visible_left_ratio = new_ratio.clamp(min_ratio, max_ratio);
77 }
78
79 fn commit_ratio(&mut self) {
80 self.left_ratio = self.visible_left_ratio;
81 }
82
83 fn on_double_click(&mut self) {
84 self.left_ratio = 0.5;
85 self.visible_left_ratio = 0.5;
86 }
87}
88
89#[derive(IntoElement)]
90pub struct SplitEditorView {
91 splittable_editor: Entity<SplittableEditor>,
92 style: EditorStyle,
93 split_state: Entity<SplitEditorState>,
94}
95
96impl SplitEditorView {
97 pub fn new(
98 splittable_editor: Entity<SplittableEditor>,
99 style: EditorStyle,
100 split_state: Entity<SplitEditorState>,
101 ) -> Self {
102 Self {
103 splittable_editor,
104 style,
105 split_state,
106 }
107 }
108}
109
110fn render_resize_handle(
111 state: &Entity<SplitEditorState>,
112 separator_color: Hsla,
113 _window: &mut Window,
114 _cx: &mut App,
115) -> AnyElement {
116 let state_for_click = state.clone();
117
118 div()
119 .id("split-resize-container")
120 .relative()
121 .h_full()
122 .flex_shrink_0()
123 .w(px(1.))
124 .bg(separator_color)
125 .child(
126 div()
127 .id("split-resize-handle")
128 .absolute()
129 .left(px(-RESIZE_HANDLE_WIDTH / 2.0))
130 .w(px(RESIZE_HANDLE_WIDTH))
131 .h_full()
132 .cursor_col_resize()
133 .block_mouse_except_scroll()
134 .on_click(move |event, _, cx| {
135 if event.click_count() >= 2 {
136 state_for_click.update(cx, |state, _| {
137 state.on_double_click();
138 });
139 }
140 cx.stop_propagation();
141 })
142 .on_drag(DraggedSplitHandle, |_, _, _, cx| cx.new(|_| gpui::Empty)),
143 )
144 .into_any_element()
145}
146
147impl RenderOnce for SplitEditorView {
148 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
149 let splittable_editor = self.splittable_editor.read(cx);
150
151 assert!(
152 splittable_editor.lhs_editor().is_some(),
153 "`SplitEditorView` requires `SplittableEditor` to be in split mode"
154 );
155
156 let lhs_editor = splittable_editor.lhs_editor().unwrap().clone();
157 let rhs_editor = splittable_editor.rhs_editor().clone();
158
159 let mut lhs = EditorElement::new(&lhs_editor, self.style.clone());
160 let mut rhs = EditorElement::new(&rhs_editor, self.style.clone());
161
162 lhs.set_split_side(SplitSide::Left);
163 rhs.set_split_side(SplitSide::Right);
164
165 let left_ratio = self.split_state.read(cx).left_ratio();
166 let right_ratio = self.split_state.read(cx).right_ratio();
167
168 let separator_color = cx.theme().colors().border_variant;
169
170 let resize_handle = render_resize_handle(&self.split_state, separator_color, window, cx);
171
172 let state_for_drag = self.split_state.downgrade();
173 let state_for_drop = self.split_state.downgrade();
174
175 let buffer_headers = SplitBufferHeadersElement::new(rhs_editor.clone(), self.style.clone());
176
177 let lhs_editor_for_order = lhs_editor;
178 let rhs_editor_for_order = rhs_editor;
179
180 div()
181 .id("split-editor-view-container")
182 .size_full()
183 .relative()
184 .child(
185 h_flex()
186 .with_dynamic_prepaint_order(move |_window, cx| {
187 let lhs_needs = lhs_editor_for_order.read(cx).has_autoscroll_request();
188 let rhs_needs = rhs_editor_for_order.read(cx).has_autoscroll_request();
189 match (lhs_needs, rhs_needs) {
190 (false, true) => smallvec![2, 1, 0],
191 _ => smallvec![0, 1, 2],
192 }
193 })
194 .id("split-editor-view")
195 .size_full()
196 .on_drag_move::<DraggedSplitHandle>(move |event, window, cx| {
197 state_for_drag
198 .update(cx, |state, cx| {
199 state.on_drag_move(event, window, cx);
200 })
201 .ok();
202 })
203 .on_drop::<DraggedSplitHandle>(move |_, _, cx| {
204 state_for_drop
205 .update(cx, |state, _| {
206 state.commit_ratio();
207 })
208 .ok();
209 })
210 .child(
211 div()
212 .id("split-editor-left")
213 .flex_shrink()
214 .min_w_0()
215 .h_full()
216 .flex_basis(DefiniteLength::Fraction(left_ratio))
217 .overflow_hidden()
218 .child(lhs),
219 )
220 .child(resize_handle)
221 .child(
222 div()
223 .id("split-editor-right")
224 .flex_shrink()
225 .min_w_0()
226 .h_full()
227 .flex_basis(DefiniteLength::Fraction(right_ratio))
228 .overflow_hidden()
229 .child(rhs),
230 ),
231 )
232 .child(buffer_headers)
233 }
234}
235
236struct SplitBufferHeadersElement {
237 editor: Entity<Editor>,
238 style: EditorStyle,
239}
240
241impl SplitBufferHeadersElement {
242 fn new(editor: Entity<Editor>, style: EditorStyle) -> Self {
243 Self { editor, style }
244 }
245}
246
247struct BufferHeaderLayout {
248 element: AnyElement,
249}
250
251struct SplitBufferHeadersPrepaintState {
252 sticky_header: Option<AnyElement>,
253 non_sticky_headers: Vec<BufferHeaderLayout>,
254}
255
256impl IntoElement for SplitBufferHeadersElement {
257 type Element = Self;
258
259 fn into_element(self) -> Self::Element {
260 self
261 }
262}
263
264impl Element for SplitBufferHeadersElement {
265 type RequestLayoutState = ();
266 type PrepaintState = SplitBufferHeadersPrepaintState;
267
268 fn id(&self) -> Option<gpui::ElementId> {
269 Some("split-buffer-headers".into())
270 }
271
272 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
273 None
274 }
275
276 fn request_layout(
277 &mut self,
278 _id: Option<&GlobalElementId>,
279 _inspector_id: Option<&InspectorElementId>,
280 window: &mut Window,
281 _cx: &mut App,
282 ) -> (LayoutId, Self::RequestLayoutState) {
283 let mut style = gpui::Style::default();
284 style.position = gpui::Position::Absolute;
285 style.inset.top = DefiniteLength::Fraction(0.0).into();
286 style.inset.left = DefiniteLength::Fraction(0.0).into();
287 style.size.width = Length::Definite(DefiniteLength::Fraction(1.0));
288 style.size.height = Length::Definite(DefiniteLength::Fraction(1.0));
289 let layout_id = window.request_layout(style, [], _cx);
290 (layout_id, ())
291 }
292
293 fn prepaint(
294 &mut self,
295 _id: Option<&GlobalElementId>,
296 _inspector_id: Option<&InspectorElementId>,
297 bounds: Bounds<Pixels>,
298 _request_layout: &mut Self::RequestLayoutState,
299 window: &mut Window,
300 cx: &mut App,
301 ) -> Self::PrepaintState {
302 if bounds.size.width <= px(0.) || bounds.size.height <= px(0.) {
303 return SplitBufferHeadersPrepaintState {
304 sticky_header: None,
305 non_sticky_headers: Vec::new(),
306 };
307 }
308
309 let rem_size = self.rem_size();
310 let text_style = TextStyleRefinement {
311 font_size: Some(self.style.text.font_size),
312 line_height: Some(self.style.text.line_height),
313 ..Default::default()
314 };
315
316 window.with_rem_size(rem_size, |window| {
317 window.with_text_style(Some(text_style), |window| {
318 Self::prepaint_inner(self, bounds, window, cx)
319 })
320 })
321 }
322
323 fn paint(
324 &mut self,
325 _id: Option<&GlobalElementId>,
326 _inspector_id: Option<&InspectorElementId>,
327 bounds: Bounds<Pixels>,
328 _request_layout: &mut Self::RequestLayoutState,
329 prepaint: &mut Self::PrepaintState,
330 window: &mut Window,
331 cx: &mut App,
332 ) {
333 let rem_size = self.rem_size();
334 let text_style = TextStyleRefinement {
335 font_size: Some(self.style.text.font_size),
336 line_height: Some(self.style.text.line_height),
337 ..Default::default()
338 };
339
340 window.with_rem_size(rem_size, |window| {
341 window.with_text_style(Some(text_style), |window| {
342 window.with_content_mask(Some(ContentMask { bounds }), |window| {
343 for header_layout in &mut prepaint.non_sticky_headers {
344 header_layout.element.paint(window, cx);
345 }
346
347 if let Some(mut sticky_header) = prepaint.sticky_header.take() {
348 sticky_header.paint(window, cx);
349 }
350 });
351 });
352 });
353 }
354}
355
356impl SplitBufferHeadersElement {
357 fn rem_size(&self) -> Option<Pixels> {
358 match self.style.text.font_size {
359 AbsoluteLength::Pixels(pixels) => {
360 let rem_size_scale = {
361 let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
362 let default_font_size_delta = 1. - default_font_size_scale;
363 1. + default_font_size_delta
364 };
365
366 Some(pixels * rem_size_scale)
367 }
368 AbsoluteLength::Rems(rems) => Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into())),
369 }
370 }
371
372 fn prepaint_inner(
373 &mut self,
374 bounds: Bounds<Pixels>,
375 window: &mut Window,
376 cx: &mut App,
377 ) -> SplitBufferHeadersPrepaintState {
378 let line_height = window.line_height();
379
380 let snapshot = self
381 .editor
382 .update(cx, |editor, cx| editor.snapshot(window, cx));
383 let scroll_position = snapshot.scroll_position();
384
385 // Compute right margin to avoid overlapping the scrollbar
386 let settings = EditorSettings::get_global(cx);
387 let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never;
388 let vertical_scrollbar_width = (scrollbars_shown
389 && settings.scrollbar.axes.vertical
390 && self.editor.read(cx).show_scrollbars.vertical)
391 .then_some(EditorElement::SCROLLBAR_WIDTH)
392 .unwrap_or_default();
393 let available_width = bounds.size.width - vertical_scrollbar_width;
394
395 let visible_height_in_lines = bounds.size.height / line_height;
396 let max_row = snapshot.max_point().row();
397 let start_row = cmp::min(DisplayRow(scroll_position.y.floor() as u32), max_row);
398 let end_row = cmp::min(
399 (scroll_position.y + visible_height_in_lines as f64).ceil() as u32,
400 max_row.next_row().0,
401 );
402 let end_row = DisplayRow(end_row);
403
404 let (selected_buffer_ids, latest_selection_anchors) =
405 self.compute_selection_info(&snapshot, cx);
406
407 let sticky_header = if snapshot.buffer_snapshot().show_headers() {
408 snapshot
409 .sticky_header_excerpt(scroll_position.y)
410 .map(|sticky_excerpt| {
411 self.build_sticky_header(
412 sticky_excerpt,
413 &snapshot,
414 scroll_position,
415 bounds,
416 available_width,
417 line_height,
418 &selected_buffer_ids,
419 &latest_selection_anchors,
420 start_row,
421 end_row,
422 window,
423 cx,
424 )
425 })
426 } else {
427 None
428 };
429
430 let sticky_header_excerpt_id = snapshot
431 .sticky_header_excerpt(scroll_position.y)
432 .map(|e| e.excerpt.id);
433
434 let non_sticky_headers = self.build_non_sticky_headers(
435 &snapshot,
436 scroll_position,
437 bounds,
438 available_width,
439 line_height,
440 start_row,
441 end_row,
442 &selected_buffer_ids,
443 &latest_selection_anchors,
444 sticky_header_excerpt_id,
445 window,
446 cx,
447 );
448
449 SplitBufferHeadersPrepaintState {
450 sticky_header,
451 non_sticky_headers,
452 }
453 }
454
455 fn compute_selection_info(
456 &self,
457 snapshot: &EditorSnapshot,
458 cx: &App,
459 ) -> (HashSet<BufferId>, HashMap<BufferId, Anchor>) {
460 let editor = self.editor.read(cx);
461 let all_selections = editor
462 .selections
463 .all::<crate::Point>(&snapshot.display_snapshot);
464 let all_anchor_selections = editor.selections.all_anchors(&snapshot.display_snapshot);
465
466 let mut selected_buffer_ids = HashSet::default();
467 for selection in &all_selections {
468 for buffer_id in snapshot
469 .buffer_snapshot()
470 .buffer_ids_for_range(selection.range())
471 {
472 selected_buffer_ids.insert(buffer_id);
473 }
474 }
475
476 let mut anchors_by_buffer: HashMap<BufferId, (usize, Anchor)> = HashMap::default();
477 for selection in all_anchor_selections.iter() {
478 let head = selection.head();
479 if let Some(buffer_id) = head.text_anchor.buffer_id {
480 anchors_by_buffer
481 .entry(buffer_id)
482 .and_modify(|(latest_id, latest_anchor)| {
483 if selection.id > *latest_id {
484 *latest_id = selection.id;
485 *latest_anchor = head;
486 }
487 })
488 .or_insert((selection.id, head));
489 }
490 }
491 let latest_selection_anchors = anchors_by_buffer
492 .into_iter()
493 .map(|(buffer_id, (_, anchor))| (buffer_id, anchor))
494 .collect();
495
496 (selected_buffer_ids, latest_selection_anchors)
497 }
498
499 fn build_sticky_header(
500 &self,
501 StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>,
502 snapshot: &EditorSnapshot,
503 scroll_position: gpui::Point<ScrollOffset>,
504 bounds: Bounds<Pixels>,
505 available_width: Pixels,
506 line_height: Pixels,
507 selected_buffer_ids: &HashSet<BufferId>,
508 latest_selection_anchors: &HashMap<BufferId, Anchor>,
509 start_row: DisplayRow,
510 end_row: DisplayRow,
511 window: &mut Window,
512 cx: &mut App,
513 ) -> AnyElement {
514 let jump_data = header_jump_data(
515 snapshot,
516 DisplayRow(scroll_position.y as u32),
517 FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
518 excerpt,
519 latest_selection_anchors,
520 );
521
522 let editor_bg_color = cx.theme().colors().editor_background;
523 let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
524
525 let mut header = v_flex()
526 .id("sticky-buffer-header")
527 .w(available_width)
528 .relative()
529 .child(
530 div()
531 .w(available_width)
532 .h(FILE_HEADER_HEIGHT as f32 * line_height)
533 .bg(linear_gradient(
534 0.,
535 linear_color_stop(editor_bg_color.opacity(0.), 0.),
536 linear_color_stop(editor_bg_color, 0.6),
537 ))
538 .absolute()
539 .top_0(),
540 )
541 .child(
542 render_buffer_header(
543 &self.editor,
544 excerpt,
545 false,
546 selected,
547 true,
548 jump_data,
549 window,
550 cx,
551 )
552 .into_any_element(),
553 )
554 .into_any_element();
555
556 let mut origin = bounds.origin;
557
558 for (block_row, block) in snapshot.blocks_in_range(start_row..end_row) {
559 if !block.is_buffer_header() {
560 continue;
561 }
562
563 if block_row.0 <= scroll_position.y as u32 {
564 continue;
565 }
566
567 let max_row = block_row.0.saturating_sub(FILE_HEADER_HEIGHT);
568 let offset = scroll_position.y - max_row as f64;
569
570 if offset > 0.0 {
571 origin.y -= Pixels::from(offset * f64::from(line_height));
572 }
573 break;
574 }
575
576 let available_size = size(
577 AvailableSpace::Definite(available_width),
578 AvailableSpace::MinContent,
579 );
580
581 header.prepaint_as_root(origin, available_size, window, cx);
582
583 header
584 }
585
586 fn build_non_sticky_headers(
587 &self,
588 snapshot: &EditorSnapshot,
589 scroll_position: gpui::Point<ScrollOffset>,
590 bounds: Bounds<Pixels>,
591 available_width: Pixels,
592 line_height: Pixels,
593 start_row: DisplayRow,
594 end_row: DisplayRow,
595 selected_buffer_ids: &HashSet<BufferId>,
596 latest_selection_anchors: &HashMap<BufferId, Anchor>,
597 sticky_header_excerpt_id: Option<ExcerptId>,
598 window: &mut Window,
599 cx: &mut App,
600 ) -> Vec<BufferHeaderLayout> {
601 let mut headers = Vec::new();
602
603 for (block_row, block) in snapshot.blocks_in_range(start_row..end_row) {
604 let (excerpt, is_folded) = match block {
605 Block::BufferHeader { excerpt, .. } => {
606 if sticky_header_excerpt_id == Some(excerpt.id) {
607 continue;
608 }
609 (excerpt, false)
610 }
611 Block::FoldedBuffer { first_excerpt, .. } => (first_excerpt, true),
612 // ExcerptBoundary is just a separator line, not a buffer header
613 Block::ExcerptBoundary { .. } | Block::Custom(_) | Block::Spacer { .. } => continue,
614 };
615
616 let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
617 let jump_data = header_jump_data(
618 snapshot,
619 block_row,
620 block.height(),
621 excerpt,
622 latest_selection_anchors,
623 );
624
625 let mut header = render_buffer_header(
626 &self.editor,
627 excerpt,
628 is_folded,
629 selected,
630 false,
631 jump_data,
632 window,
633 cx,
634 )
635 .into_any_element();
636
637 let y_offset = (block_row.0 as f64 - scroll_position.y) * f64::from(line_height);
638 let origin = point(bounds.origin.x, bounds.origin.y + Pixels::from(y_offset));
639
640 let available_size = size(
641 AvailableSpace::Definite(available_width),
642 AvailableSpace::MinContent,
643 );
644
645 header.prepaint_as_root(origin, available_size, window, cx);
646
647 headers.push(BufferHeaderLayout { element: header });
648 }
649
650 headers
651 }
652}