1use crate::{
2 blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
3 display_map::{
4 BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint, TransformBlock,
5 },
6 editor_settings::{
7 CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar,
8 },
9 git::{
10 blame::{CommitDetails, GitBlame},
11 diff_hunk_to_display, DisplayDiffHunk,
12 },
13 hover_popover::{
14 self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
15 },
16 hunk_status,
17 items::BufferSearchHighlights,
18 mouse_context_menu::{self, MouseContextMenu},
19 scroll::scroll_amount::ScrollAmount,
20 CodeActionsMenu, CursorShape, DisplayPoint, DisplayRow, DocumentHighlightRead,
21 DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
22 ExpandExcerpts, GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HunkToExpand,
23 LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase,
24 Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
25};
26use client::ParticipantIndex;
27use collections::{BTreeMap, HashMap};
28use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
29use gpui::{
30 anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
31 transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
32 ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
33 FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent,
34 MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels,
35 ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
36 Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, ViewContext, WeakView,
37 WindowContext,
38};
39use itertools::Itertools;
40use language::language_settings::{
41 IndentGuideBackgroundColoring, IndentGuideColoring, ShowWhitespaceSetting,
42};
43use lsp::DiagnosticSeverity;
44use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
45use project::{
46 project_settings::{GitGutterSetting, ProjectSettings},
47 ProjectPath,
48};
49use settings::Settings;
50use smallvec::{smallvec, SmallVec};
51use std::{
52 any::TypeId,
53 borrow::Cow,
54 cmp::{self, Ordering},
55 fmt::{self, Write},
56 iter, mem,
57 ops::{Deref, Range},
58 sync::Arc,
59};
60use sum_tree::Bias;
61use theme::{ActiveTheme, PlayerColor};
62use ui::prelude::*;
63use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
64use util::ResultExt;
65use workspace::{item::Item, Workspace};
66
67struct SelectionLayout {
68 head: DisplayPoint,
69 cursor_shape: CursorShape,
70 is_newest: bool,
71 is_local: bool,
72 range: Range<DisplayPoint>,
73 active_rows: Range<DisplayRow>,
74 user_name: Option<SharedString>,
75}
76
77impl SelectionLayout {
78 fn new<T: ToPoint + ToDisplayPoint + Clone>(
79 selection: Selection<T>,
80 line_mode: bool,
81 cursor_shape: CursorShape,
82 map: &DisplaySnapshot,
83 is_newest: bool,
84 is_local: bool,
85 user_name: Option<SharedString>,
86 ) -> Self {
87 let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
88 let display_selection = point_selection.map(|p| p.to_display_point(map));
89 let mut range = display_selection.range();
90 let mut head = display_selection.head();
91 let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
92 ..map.next_line_boundary(point_selection.end).1.row();
93
94 // vim visual line mode
95 if line_mode {
96 let point_range = map.expand_to_line(point_selection.range());
97 range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
98 }
99
100 // any vim visual mode (including line mode)
101 if (cursor_shape == CursorShape::Block || cursor_shape == CursorShape::Hollow)
102 && !range.is_empty()
103 && !selection.reversed
104 {
105 if head.column() > 0 {
106 head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
107 } else if head.row().0 > 0 && head != map.max_point() {
108 head = map.clip_point(
109 DisplayPoint::new(
110 head.row().previous_row(),
111 map.line_len(head.row().previous_row()),
112 ),
113 Bias::Left,
114 );
115 // updating range.end is a no-op unless you're cursor is
116 // on the newline containing a multi-buffer divider
117 // in which case the clip_point may have moved the head up
118 // an additional row.
119 range.end = DisplayPoint::new(head.row().next_row(), 0);
120 active_rows.end = head.row();
121 }
122 }
123
124 Self {
125 head,
126 cursor_shape,
127 is_newest,
128 is_local,
129 range,
130 active_rows,
131 user_name,
132 }
133 }
134}
135
136pub struct EditorElement {
137 editor: View<Editor>,
138 style: EditorStyle,
139}
140
141type DisplayRowDelta = u32;
142
143impl EditorElement {
144 pub(crate) const SCROLLBAR_WIDTH: Pixels = px(13.);
145
146 pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
147 Self {
148 editor: editor.clone(),
149 style,
150 }
151 }
152
153 fn register_actions(&self, cx: &mut WindowContext) {
154 let view = &self.editor;
155 view.update(cx, |editor, cx| {
156 for action in editor.editor_actions.iter() {
157 (action)(cx)
158 }
159 });
160
161 crate::rust_analyzer_ext::apply_related_actions(view, cx);
162 register_action(view, cx, Editor::move_left);
163 register_action(view, cx, Editor::move_right);
164 register_action(view, cx, Editor::move_down);
165 register_action(view, cx, Editor::move_down_by_lines);
166 register_action(view, cx, Editor::select_down_by_lines);
167 register_action(view, cx, Editor::move_up);
168 register_action(view, cx, Editor::move_up_by_lines);
169 register_action(view, cx, Editor::select_up_by_lines);
170 register_action(view, cx, Editor::cancel);
171 register_action(view, cx, Editor::newline);
172 register_action(view, cx, Editor::newline_above);
173 register_action(view, cx, Editor::newline_below);
174 register_action(view, cx, Editor::backspace);
175 register_action(view, cx, Editor::delete);
176 register_action(view, cx, Editor::tab);
177 register_action(view, cx, Editor::tab_prev);
178 register_action(view, cx, Editor::indent);
179 register_action(view, cx, Editor::outdent);
180 register_action(view, cx, Editor::delete_line);
181 register_action(view, cx, Editor::join_lines);
182 register_action(view, cx, Editor::sort_lines_case_sensitive);
183 register_action(view, cx, Editor::sort_lines_case_insensitive);
184 register_action(view, cx, Editor::reverse_lines);
185 register_action(view, cx, Editor::shuffle_lines);
186 register_action(view, cx, Editor::convert_to_upper_case);
187 register_action(view, cx, Editor::convert_to_lower_case);
188 register_action(view, cx, Editor::convert_to_title_case);
189 register_action(view, cx, Editor::convert_to_snake_case);
190 register_action(view, cx, Editor::convert_to_kebab_case);
191 register_action(view, cx, Editor::convert_to_upper_camel_case);
192 register_action(view, cx, Editor::convert_to_lower_camel_case);
193 register_action(view, cx, Editor::convert_to_opposite_case);
194 register_action(view, cx, Editor::delete_to_previous_word_start);
195 register_action(view, cx, Editor::delete_to_previous_subword_start);
196 register_action(view, cx, Editor::delete_to_next_word_end);
197 register_action(view, cx, Editor::delete_to_next_subword_end);
198 register_action(view, cx, Editor::delete_to_beginning_of_line);
199 register_action(view, cx, Editor::delete_to_end_of_line);
200 register_action(view, cx, Editor::cut_to_end_of_line);
201 register_action(view, cx, Editor::duplicate_line_up);
202 register_action(view, cx, Editor::duplicate_line_down);
203 register_action(view, cx, Editor::move_line_up);
204 register_action(view, cx, Editor::move_line_down);
205 register_action(view, cx, Editor::transpose);
206 register_action(view, cx, Editor::cut);
207 register_action(view, cx, Editor::copy);
208 register_action(view, cx, Editor::paste);
209 register_action(view, cx, Editor::undo);
210 register_action(view, cx, Editor::redo);
211 register_action(view, cx, Editor::move_page_up);
212 register_action(view, cx, Editor::move_page_down);
213 register_action(view, cx, Editor::next_screen);
214 register_action(view, cx, Editor::scroll_cursor_top);
215 register_action(view, cx, Editor::scroll_cursor_center);
216 register_action(view, cx, Editor::scroll_cursor_bottom);
217 register_action(view, cx, |editor, _: &LineDown, cx| {
218 editor.scroll_screen(&ScrollAmount::Line(1.), cx)
219 });
220 register_action(view, cx, |editor, _: &LineUp, cx| {
221 editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
222 });
223 register_action(view, cx, |editor, _: &HalfPageDown, cx| {
224 editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
225 });
226 register_action(view, cx, |editor, _: &HalfPageUp, cx| {
227 editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
228 });
229 register_action(view, cx, |editor, _: &PageDown, cx| {
230 editor.scroll_screen(&ScrollAmount::Page(1.), cx)
231 });
232 register_action(view, cx, |editor, _: &PageUp, cx| {
233 editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
234 });
235 register_action(view, cx, Editor::move_to_previous_word_start);
236 register_action(view, cx, Editor::move_to_previous_subword_start);
237 register_action(view, cx, Editor::move_to_next_word_end);
238 register_action(view, cx, Editor::move_to_next_subword_end);
239 register_action(view, cx, Editor::move_to_beginning_of_line);
240 register_action(view, cx, Editor::move_to_end_of_line);
241 register_action(view, cx, Editor::move_to_start_of_paragraph);
242 register_action(view, cx, Editor::move_to_end_of_paragraph);
243 register_action(view, cx, Editor::move_to_beginning);
244 register_action(view, cx, Editor::move_to_end);
245 register_action(view, cx, Editor::select_up);
246 register_action(view, cx, Editor::select_down);
247 register_action(view, cx, Editor::select_left);
248 register_action(view, cx, Editor::select_right);
249 register_action(view, cx, Editor::select_to_previous_word_start);
250 register_action(view, cx, Editor::select_to_previous_subword_start);
251 register_action(view, cx, Editor::select_to_next_word_end);
252 register_action(view, cx, Editor::select_to_next_subword_end);
253 register_action(view, cx, Editor::select_to_beginning_of_line);
254 register_action(view, cx, Editor::select_to_end_of_line);
255 register_action(view, cx, Editor::select_to_start_of_paragraph);
256 register_action(view, cx, Editor::select_to_end_of_paragraph);
257 register_action(view, cx, Editor::select_to_beginning);
258 register_action(view, cx, Editor::select_to_end);
259 register_action(view, cx, Editor::select_all);
260 register_action(view, cx, |editor, action, cx| {
261 editor.select_all_matches(action, cx).log_err();
262 });
263 register_action(view, cx, Editor::select_line);
264 register_action(view, cx, Editor::split_selection_into_lines);
265 register_action(view, cx, Editor::add_selection_above);
266 register_action(view, cx, Editor::add_selection_below);
267 register_action(view, cx, |editor, action, cx| {
268 editor.select_next(action, cx).log_err();
269 });
270 register_action(view, cx, |editor, action, cx| {
271 editor.select_previous(action, cx).log_err();
272 });
273 register_action(view, cx, Editor::toggle_comments);
274 register_action(view, cx, Editor::select_larger_syntax_node);
275 register_action(view, cx, Editor::select_smaller_syntax_node);
276 register_action(view, cx, Editor::move_to_enclosing_bracket);
277 register_action(view, cx, Editor::undo_selection);
278 register_action(view, cx, Editor::redo_selection);
279 if !view.read(cx).is_singleton(cx) {
280 register_action(view, cx, Editor::expand_excerpts);
281 }
282 register_action(view, cx, Editor::go_to_diagnostic);
283 register_action(view, cx, Editor::go_to_prev_diagnostic);
284 register_action(view, cx, Editor::go_to_hunk);
285 register_action(view, cx, Editor::go_to_prev_hunk);
286 register_action(view, cx, |editor, a, cx| {
287 editor.go_to_definition(a, cx).detach_and_log_err(cx);
288 });
289 register_action(view, cx, |editor, a, cx| {
290 editor.go_to_definition_split(a, cx).detach_and_log_err(cx);
291 });
292 register_action(view, cx, |editor, a, cx| {
293 editor.go_to_implementation(a, cx).detach_and_log_err(cx);
294 });
295 register_action(view, cx, |editor, a, cx| {
296 editor
297 .go_to_implementation_split(a, cx)
298 .detach_and_log_err(cx);
299 });
300 register_action(view, cx, |editor, a, cx| {
301 editor.go_to_type_definition(a, cx).detach_and_log_err(cx);
302 });
303 register_action(view, cx, |editor, a, cx| {
304 editor
305 .go_to_type_definition_split(a, cx)
306 .detach_and_log_err(cx);
307 });
308 register_action(view, cx, Editor::open_url);
309 register_action(view, cx, Editor::fold);
310 register_action(view, cx, Editor::fold_at);
311 register_action(view, cx, Editor::unfold_lines);
312 register_action(view, cx, Editor::unfold_at);
313 register_action(view, cx, Editor::fold_selected_ranges);
314 register_action(view, cx, Editor::show_completions);
315 register_action(view, cx, Editor::toggle_code_actions);
316 register_action(view, cx, Editor::open_excerpts);
317 register_action(view, cx, Editor::open_excerpts_in_split);
318 register_action(view, cx, Editor::toggle_soft_wrap);
319 register_action(view, cx, Editor::toggle_line_numbers);
320 register_action(view, cx, Editor::toggle_indent_guides);
321 register_action(view, cx, Editor::toggle_inlay_hints);
322 register_action(view, cx, hover_popover::hover);
323 register_action(view, cx, Editor::reveal_in_finder);
324 register_action(view, cx, Editor::copy_path);
325 register_action(view, cx, Editor::copy_relative_path);
326 register_action(view, cx, Editor::copy_highlight_json);
327 register_action(view, cx, Editor::copy_permalink_to_line);
328 register_action(view, cx, Editor::open_permalink_to_line);
329 register_action(view, cx, Editor::toggle_git_blame);
330 register_action(view, cx, Editor::toggle_git_blame_inline);
331 register_action(view, cx, Editor::toggle_hunk_diff);
332 register_action(view, cx, Editor::expand_all_hunk_diffs);
333 register_action(view, cx, |editor, action, cx| {
334 if let Some(task) = editor.format(action, cx) {
335 task.detach_and_log_err(cx);
336 } else {
337 cx.propagate();
338 }
339 });
340 register_action(view, cx, Editor::restart_language_server);
341 register_action(view, cx, Editor::show_character_palette);
342 register_action(view, cx, |editor, action, cx| {
343 if let Some(task) = editor.confirm_completion(action, cx) {
344 task.detach_and_log_err(cx);
345 } else {
346 cx.propagate();
347 }
348 });
349 register_action(view, cx, |editor, action, cx| {
350 if let Some(task) = editor.confirm_code_action(action, cx) {
351 task.detach_and_log_err(cx);
352 } else {
353 cx.propagate();
354 }
355 });
356 register_action(view, cx, |editor, action, cx| {
357 if let Some(task) = editor.rename(action, cx) {
358 task.detach_and_log_err(cx);
359 } else {
360 cx.propagate();
361 }
362 });
363 register_action(view, cx, |editor, action, cx| {
364 if let Some(task) = editor.confirm_rename(action, cx) {
365 task.detach_and_log_err(cx);
366 } else {
367 cx.propagate();
368 }
369 });
370 register_action(view, cx, |editor, action, cx| {
371 if let Some(task) = editor.find_all_references(action, cx) {
372 task.detach_and_log_err(cx);
373 } else {
374 cx.propagate();
375 }
376 });
377 register_action(view, cx, Editor::next_inline_completion);
378 register_action(view, cx, Editor::previous_inline_completion);
379 register_action(view, cx, Editor::show_inline_completion);
380 register_action(view, cx, Editor::context_menu_first);
381 register_action(view, cx, Editor::context_menu_prev);
382 register_action(view, cx, Editor::context_menu_next);
383 register_action(view, cx, Editor::context_menu_last);
384 register_action(view, cx, Editor::display_cursor_names);
385 register_action(view, cx, Editor::unique_lines_case_insensitive);
386 register_action(view, cx, Editor::unique_lines_case_sensitive);
387 register_action(view, cx, Editor::accept_partial_inline_completion);
388 register_action(view, cx, Editor::accept_inline_completion);
389 register_action(view, cx, Editor::revert_selected_hunks);
390 register_action(view, cx, Editor::open_active_item_in_terminal)
391 }
392
393 fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
394 let position_map = layout.position_map.clone();
395 cx.on_key_event({
396 let editor = self.editor.clone();
397 let text_hitbox = layout.text_hitbox.clone();
398 move |event: &ModifiersChangedEvent, phase, cx| {
399 if phase != DispatchPhase::Bubble {
400 return;
401 }
402
403 editor.update(cx, |editor, cx| {
404 Self::modifiers_changed(editor, event, &position_map, &text_hitbox, cx)
405 })
406 }
407 });
408 }
409
410 fn modifiers_changed(
411 editor: &mut Editor,
412 event: &ModifiersChangedEvent,
413 position_map: &PositionMap,
414 text_hitbox: &Hitbox,
415 cx: &mut ViewContext<Editor>,
416 ) {
417 let mouse_position = cx.mouse_position();
418 if !text_hitbox.is_hovered(cx) {
419 return;
420 }
421
422 editor.update_hovered_link(
423 position_map.point_for_position(text_hitbox.bounds, mouse_position),
424 &position_map.snapshot,
425 event.modifiers,
426 cx,
427 )
428 }
429
430 fn mouse_left_down(
431 editor: &mut Editor,
432 event: &MouseDownEvent,
433 hovered_hunk: Option<&HunkToExpand>,
434 position_map: &PositionMap,
435 text_hitbox: &Hitbox,
436 gutter_hitbox: &Hitbox,
437 cx: &mut ViewContext<Editor>,
438 ) {
439 if cx.default_prevented() {
440 return;
441 }
442
443 let mut click_count = event.click_count;
444 let mut modifiers = event.modifiers;
445
446 if let Some(hovered_hunk) = hovered_hunk {
447 editor.expand_diff_hunk(None, hovered_hunk, cx);
448 cx.notify();
449 return;
450 } else if gutter_hitbox.is_hovered(cx) {
451 click_count = 3; // Simulate triple-click when clicking the gutter to select lines
452 } else if !text_hitbox.is_hovered(cx) {
453 return;
454 }
455
456 if click_count == 2 && !editor.buffer().read(cx).is_singleton() {
457 match EditorSettings::get_global(cx).double_click_in_multibuffer {
458 DoubleClickInMultibuffer::Select => {
459 // do nothing special on double click, all selection logic is below
460 }
461 DoubleClickInMultibuffer::Open => {
462 if modifiers.alt {
463 // if double click is made with alt, pretend it's a regular double click without opening and alt,
464 // and run the selection logic.
465 modifiers.alt = false;
466 } else {
467 // if double click is made without alt, open the corresponding excerp
468 editor.open_excerpts(&OpenExcerpts, cx);
469 return;
470 }
471 }
472 }
473 }
474
475 let point_for_position =
476 position_map.point_for_position(text_hitbox.bounds, event.position);
477 let position = point_for_position.previous_valid;
478 if modifiers.shift && modifiers.alt {
479 editor.select(
480 SelectPhase::BeginColumnar {
481 position,
482 reset: false,
483 goal_column: point_for_position.exact_unclipped.column(),
484 },
485 cx,
486 );
487 } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.secondary()
488 {
489 editor.select(
490 SelectPhase::Extend {
491 position,
492 click_count,
493 },
494 cx,
495 );
496 } else {
497 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
498 let multi_cursor_modifier = match multi_cursor_setting {
499 MultiCursorModifier::Alt => modifiers.alt,
500 MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
501 };
502 editor.select(
503 SelectPhase::Begin {
504 position,
505 add: multi_cursor_modifier,
506 click_count,
507 },
508 cx,
509 );
510 }
511
512 cx.stop_propagation();
513 }
514
515 fn mouse_right_down(
516 editor: &mut Editor,
517 event: &MouseDownEvent,
518 position_map: &PositionMap,
519 text_hitbox: &Hitbox,
520 cx: &mut ViewContext<Editor>,
521 ) {
522 if !text_hitbox.is_hovered(cx) {
523 return;
524 }
525 let point_for_position =
526 position_map.point_for_position(text_hitbox.bounds, event.position);
527 mouse_context_menu::deploy_context_menu(
528 editor,
529 event.position,
530 point_for_position.previous_valid,
531 cx,
532 );
533 cx.stop_propagation();
534 }
535
536 fn mouse_middle_down(
537 editor: &mut Editor,
538 event: &MouseDownEvent,
539 position_map: &PositionMap,
540 text_hitbox: &Hitbox,
541 cx: &mut ViewContext<Editor>,
542 ) {
543 if cx.default_prevented() {
544 return;
545 }
546
547 let point_for_position =
548 position_map.point_for_position(text_hitbox.bounds, event.position);
549 let position = point_for_position.previous_valid;
550
551 editor.select(
552 SelectPhase::BeginColumnar {
553 position,
554 reset: true,
555 goal_column: point_for_position.exact_unclipped.column(),
556 },
557 cx,
558 );
559 }
560
561 fn mouse_up(
562 editor: &mut Editor,
563 event: &MouseUpEvent,
564 position_map: &PositionMap,
565 text_hitbox: &Hitbox,
566 cx: &mut ViewContext<Editor>,
567 ) {
568 let end_selection = editor.has_pending_selection();
569 let pending_nonempty_selections = editor.has_pending_nonempty_selection();
570
571 if end_selection {
572 editor.select(SelectPhase::End, cx);
573 }
574
575 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
576 let multi_cursor_modifier = match multi_cursor_setting {
577 MultiCursorModifier::Alt => event.modifiers.secondary(),
578 MultiCursorModifier::CmdOrCtrl => event.modifiers.alt,
579 };
580
581 if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(cx) {
582 let point = position_map.point_for_position(text_hitbox.bounds, event.position);
583 editor.handle_click_hovered_link(point, event.modifiers, cx);
584
585 cx.stop_propagation();
586 } else if end_selection {
587 cx.stop_propagation();
588 } else if cfg!(target_os = "linux") && event.button == MouseButton::Middle {
589 if !text_hitbox.is_hovered(cx) || editor.read_only(cx) {
590 return;
591 }
592
593 #[cfg(target_os = "linux")]
594 if let Some(item) = cx.read_from_clipboard() {
595 let point_for_position =
596 position_map.point_for_position(text_hitbox.bounds, event.position);
597 let position = point_for_position.previous_valid;
598
599 editor.select(
600 SelectPhase::Begin {
601 position,
602 add: false,
603 click_count: 1,
604 },
605 cx,
606 );
607 editor.insert(item.text(), cx);
608 }
609 cx.stop_propagation()
610 }
611 }
612
613 fn mouse_dragged(
614 editor: &mut Editor,
615 event: &MouseMoveEvent,
616 position_map: &PositionMap,
617 text_bounds: Bounds<Pixels>,
618 cx: &mut ViewContext<Editor>,
619 ) {
620 if !editor.has_pending_selection() {
621 return;
622 }
623
624 let point_for_position = position_map.point_for_position(text_bounds, event.position);
625 let mut scroll_delta = gpui::Point::<f32>::default();
626 let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
627 let top = text_bounds.origin.y + vertical_margin;
628 let bottom = text_bounds.lower_left().y - vertical_margin;
629 if event.position.y < top {
630 scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);
631 }
632 if event.position.y > bottom {
633 scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);
634 }
635
636 let horizontal_margin = position_map.line_height.min(text_bounds.size.width / 3.0);
637 let left = text_bounds.origin.x + horizontal_margin;
638 let right = text_bounds.upper_right().x - horizontal_margin;
639 if event.position.x < left {
640 scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
641 }
642 if event.position.x > right {
643 scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
644 }
645
646 editor.select(
647 SelectPhase::Update {
648 position: point_for_position.previous_valid,
649 goal_column: point_for_position.exact_unclipped.column(),
650 scroll_delta,
651 },
652 cx,
653 );
654 }
655
656 fn mouse_moved(
657 editor: &mut Editor,
658 event: &MouseMoveEvent,
659 position_map: &PositionMap,
660 text_hitbox: &Hitbox,
661 gutter_hitbox: &Hitbox,
662 cx: &mut ViewContext<Editor>,
663 ) {
664 let modifiers = event.modifiers;
665 let gutter_hovered = gutter_hitbox.is_hovered(cx);
666 editor.set_gutter_hovered(gutter_hovered, cx);
667
668 // Don't trigger hover popover if mouse is hovering over context menu
669 if text_hitbox.is_hovered(cx) {
670 let point_for_position =
671 position_map.point_for_position(text_hitbox.bounds, event.position);
672
673 editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
674
675 if let Some(point) = point_for_position.as_valid() {
676 let anchor = position_map
677 .snapshot
678 .buffer_snapshot
679 .anchor_before(point.to_offset(&position_map.snapshot, Bias::Left));
680 hover_at(editor, Some(anchor), cx);
681 Self::update_visible_cursor(editor, point, position_map, cx);
682 } else {
683 hover_at(editor, None, cx);
684 }
685 } else {
686 editor.hide_hovered_link(cx);
687 hover_at(editor, None, cx);
688 if gutter_hovered {
689 cx.stop_propagation();
690 }
691 }
692 }
693
694 fn update_visible_cursor(
695 editor: &mut Editor,
696 point: DisplayPoint,
697 position_map: &PositionMap,
698 cx: &mut ViewContext<Editor>,
699 ) {
700 let snapshot = &position_map.snapshot;
701 let Some(hub) = editor.collaboration_hub() else {
702 return;
703 };
704 let range = DisplayPoint::new(point.row(), point.column().saturating_sub(1))
705 ..DisplayPoint::new(
706 point.row(),
707 (point.column() + 1).min(snapshot.line_len(point.row())),
708 );
709
710 let range = snapshot
711 .buffer_snapshot
712 .anchor_at(range.start.to_point(&snapshot.display_snapshot), Bias::Left)
713 ..snapshot
714 .buffer_snapshot
715 .anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right);
716
717 let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
718 return;
719 };
720 let key = crate::HoveredCursor {
721 replica_id: selection.replica_id,
722 selection_id: selection.selection.id,
723 };
724 editor.hovered_cursors.insert(
725 key.clone(),
726 cx.spawn(|editor, mut cx| async move {
727 cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
728 editor
729 .update(&mut cx, |editor, cx| {
730 editor.hovered_cursors.remove(&key);
731 cx.notify();
732 })
733 .ok();
734 }),
735 );
736 cx.notify()
737 }
738
739 fn layout_selections(
740 &self,
741 start_anchor: Anchor,
742 end_anchor: Anchor,
743 snapshot: &EditorSnapshot,
744 start_row: DisplayRow,
745 end_row: DisplayRow,
746 cx: &mut WindowContext,
747 ) -> (
748 Vec<(PlayerColor, Vec<SelectionLayout>)>,
749 BTreeMap<DisplayRow, bool>,
750 Option<DisplayPoint>,
751 ) {
752 let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
753 let mut active_rows = BTreeMap::new();
754 let mut newest_selection_head = None;
755 let editor = self.editor.read(cx);
756
757 if editor.show_local_selections {
758 let mut local_selections: Vec<Selection<Point>> = editor
759 .selections
760 .disjoint_in_range(start_anchor..end_anchor, cx);
761 local_selections.extend(editor.selections.pending(cx));
762 let mut layouts = Vec::new();
763 let newest = editor.selections.newest(cx);
764 for selection in local_selections.drain(..) {
765 let is_empty = selection.start == selection.end;
766 let is_newest = selection == newest;
767
768 let layout = SelectionLayout::new(
769 selection,
770 editor.selections.line_mode,
771 editor.cursor_shape,
772 &snapshot.display_snapshot,
773 is_newest,
774 editor.leader_peer_id.is_none(),
775 None,
776 );
777 if is_newest {
778 newest_selection_head = Some(layout.head);
779 }
780
781 for row in cmp::max(layout.active_rows.start.0, start_row.0)
782 ..=cmp::min(layout.active_rows.end.0, end_row.0)
783 {
784 let contains_non_empty_selection =
785 active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
786 *contains_non_empty_selection |= !is_empty;
787 }
788 layouts.push(layout);
789 }
790
791 let player = if editor.read_only(cx) {
792 cx.theme().players().read_only()
793 } else {
794 self.style.local_player
795 };
796
797 selections.push((player, layouts));
798 }
799
800 if let Some(collaboration_hub) = &editor.collaboration_hub {
801 // When following someone, render the local selections in their color.
802 if let Some(leader_id) = editor.leader_peer_id {
803 if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
804 if let Some(participant_index) = collaboration_hub
805 .user_participant_indices(cx)
806 .get(&collaborator.user_id)
807 {
808 if let Some((local_selection_style, _)) = selections.first_mut() {
809 *local_selection_style = cx
810 .theme()
811 .players()
812 .color_for_participant(participant_index.0);
813 }
814 }
815 }
816 }
817
818 let mut remote_selections = HashMap::default();
819 for selection in snapshot.remote_selections_in_range(
820 &(start_anchor..end_anchor),
821 collaboration_hub.as_ref(),
822 cx,
823 ) {
824 let selection_style = Self::get_participant_color(selection.participant_index, cx);
825
826 // Don't re-render the leader's selections, since the local selections
827 // match theirs.
828 if Some(selection.peer_id) == editor.leader_peer_id {
829 continue;
830 }
831 let key = HoveredCursor {
832 replica_id: selection.replica_id,
833 selection_id: selection.selection.id,
834 };
835
836 let is_shown =
837 editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
838
839 remote_selections
840 .entry(selection.replica_id)
841 .or_insert((selection_style, Vec::new()))
842 .1
843 .push(SelectionLayout::new(
844 selection.selection,
845 selection.line_mode,
846 selection.cursor_shape,
847 &snapshot.display_snapshot,
848 false,
849 false,
850 if is_shown { selection.user_name } else { None },
851 ));
852 }
853
854 selections.extend(remote_selections.into_values());
855 }
856 (selections, active_rows, newest_selection_head)
857 }
858
859 fn collect_cursors(
860 &self,
861 snapshot: &EditorSnapshot,
862 cx: &mut WindowContext,
863 ) -> Vec<(DisplayPoint, Hsla)> {
864 let editor = self.editor.read(cx);
865 let mut cursors = Vec::new();
866 let mut skip_local = false;
867 let mut add_cursor = |anchor: Anchor, color| {
868 cursors.push((anchor.to_display_point(&snapshot.display_snapshot), color));
869 };
870 // Remote cursors
871 if let Some(collaboration_hub) = &editor.collaboration_hub {
872 for remote_selection in snapshot.remote_selections_in_range(
873 &(Anchor::min()..Anchor::max()),
874 collaboration_hub.deref(),
875 cx,
876 ) {
877 let color = Self::get_participant_color(remote_selection.participant_index, cx);
878 add_cursor(remote_selection.selection.head(), color.cursor);
879 if Some(remote_selection.peer_id) == editor.leader_peer_id {
880 skip_local = true;
881 }
882 }
883 }
884 // Local cursors
885 if !skip_local {
886 let color = cx.theme().players().local().cursor;
887 editor.selections.disjoint.iter().for_each(|selection| {
888 add_cursor(selection.head(), color);
889 });
890 if let Some(ref selection) = editor.selections.pending_anchor() {
891 add_cursor(selection.head(), color);
892 }
893 }
894 cursors
895 }
896
897 #[allow(clippy::too_many_arguments)]
898 fn layout_visible_cursors(
899 &self,
900 snapshot: &EditorSnapshot,
901 selections: &[(PlayerColor, Vec<SelectionLayout>)],
902 visible_display_row_range: Range<DisplayRow>,
903 line_layouts: &[LineWithInvisibles],
904 text_hitbox: &Hitbox,
905 content_origin: gpui::Point<Pixels>,
906 scroll_position: gpui::Point<f32>,
907 scroll_pixel_position: gpui::Point<Pixels>,
908 line_height: Pixels,
909 em_width: Pixels,
910 autoscroll_containing_element: bool,
911 cx: &mut WindowContext,
912 ) -> Vec<CursorLayout> {
913 let mut autoscroll_bounds = None;
914 let cursor_layouts = self.editor.update(cx, |editor, cx| {
915 let mut cursors = Vec::new();
916 for (player_color, selections) in selections {
917 for selection in selections {
918 let cursor_position = selection.head;
919
920 let in_range = visible_display_row_range.contains(&cursor_position.row());
921 if (selection.is_local && !editor.show_local_cursors(cx)) || !in_range {
922 continue;
923 }
924
925 let cursor_row_layout = &line_layouts
926 [cursor_position.row().minus(visible_display_row_range.start) as usize];
927 let cursor_column = cursor_position.column() as usize;
928
929 let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
930 let mut block_width =
931 cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
932 if block_width == Pixels::ZERO {
933 block_width = em_width;
934 }
935 let block_text = if let CursorShape::Block = selection.cursor_shape {
936 snapshot.display_chars_at(cursor_position).next().and_then(
937 |(character, _)| {
938 let text = if character == '\n' {
939 SharedString::from(" ")
940 } else {
941 SharedString::from(character.to_string())
942 };
943 let len = text.len();
944
945 let font = cursor_row_layout
946 .font_id_for_index(cursor_column)
947 .and_then(|cursor_font_id| {
948 cx.text_system().get_font_for_id(cursor_font_id)
949 })
950 .unwrap_or(self.style.text.font());
951
952 cx.text_system()
953 .shape_line(
954 text,
955 cursor_row_layout.font_size,
956 &[TextRun {
957 len,
958 font,
959 color: self.style.background,
960 background_color: None,
961 strikethrough: None,
962 underline: None,
963 }],
964 )
965 .log_err()
966 },
967 )
968 } else {
969 None
970 };
971
972 let x = cursor_character_x - scroll_pixel_position.x;
973 let y = (cursor_position.row().as_f32()
974 - scroll_pixel_position.y / line_height)
975 * line_height;
976 if selection.is_newest {
977 editor.pixel_position_of_newest_cursor = Some(point(
978 text_hitbox.origin.x + x + block_width / 2.,
979 text_hitbox.origin.y + y + line_height / 2.,
980 ));
981
982 if autoscroll_containing_element {
983 let top = text_hitbox.origin.y
984 + (cursor_position.row().as_f32() - scroll_position.y - 3.).max(0.)
985 * line_height;
986 let left = text_hitbox.origin.x
987 + (cursor_position.column() as f32 - scroll_position.x - 3.)
988 .max(0.)
989 * em_width;
990
991 let bottom = text_hitbox.origin.y
992 + (cursor_position.row().as_f32() - scroll_position.y + 4.)
993 * line_height;
994 let right = text_hitbox.origin.x
995 + (cursor_position.column() as f32 - scroll_position.x + 4.)
996 * em_width;
997
998 autoscroll_bounds =
999 Some(Bounds::from_corners(point(left, top), point(right, bottom)))
1000 }
1001 }
1002
1003 let mut cursor = CursorLayout {
1004 color: player_color.cursor,
1005 block_width,
1006 origin: point(x, y),
1007 line_height,
1008 shape: selection.cursor_shape,
1009 block_text,
1010 cursor_name: None,
1011 };
1012 let cursor_name = selection.user_name.clone().map(|name| CursorName {
1013 string: name,
1014 color: self.style.background,
1015 is_top_row: cursor_position.row().0 == 0,
1016 });
1017 cursor.layout(content_origin, cursor_name, cx);
1018 cursors.push(cursor);
1019 }
1020 }
1021 cursors
1022 });
1023
1024 if let Some(bounds) = autoscroll_bounds {
1025 cx.request_autoscroll(bounds);
1026 }
1027
1028 cursor_layouts
1029 }
1030
1031 fn layout_scrollbar(
1032 &self,
1033 snapshot: &EditorSnapshot,
1034 bounds: Bounds<Pixels>,
1035 scroll_position: gpui::Point<f32>,
1036 rows_per_page: f32,
1037 non_visible_cursors: bool,
1038 cx: &mut WindowContext,
1039 ) -> Option<ScrollbarLayout> {
1040 let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
1041 let show_scrollbars = match scrollbar_settings.show {
1042 ShowScrollbar::Auto => {
1043 let editor = self.editor.read(cx);
1044 let is_singleton = editor.is_singleton(cx);
1045 // Git
1046 (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
1047 ||
1048 // Buffer Search Results
1049 (is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
1050 ||
1051 // Selected Symbol Occurrences
1052 (is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
1053 ||
1054 // Diagnostics
1055 (is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
1056 ||
1057 // Cursors out of sight
1058 non_visible_cursors
1059 ||
1060 // Scrollmanager
1061 editor.scroll_manager.scrollbars_visible()
1062 }
1063 ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
1064 ShowScrollbar::Always => true,
1065 ShowScrollbar::Never => false,
1066 };
1067 if snapshot.mode != EditorMode::Full {
1068 return None;
1069 }
1070
1071 let visible_row_range = scroll_position.y..scroll_position.y + rows_per_page;
1072
1073 // If a drag took place after we started dragging the scrollbar,
1074 // cancel the scrollbar drag.
1075 if cx.has_active_drag() {
1076 self.editor.update(cx, |editor, cx| {
1077 editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
1078 });
1079 }
1080
1081 let track_bounds = Bounds::from_corners(
1082 point(self.scrollbar_left(&bounds), bounds.origin.y),
1083 point(bounds.lower_right().x, bounds.lower_left().y),
1084 );
1085
1086 let height = bounds.size.height;
1087 let total_rows = snapshot.max_point().row().as_f32() + rows_per_page;
1088 let px_per_row = height / total_rows;
1089 let thumb_height = (rows_per_page * px_per_row).max(ScrollbarLayout::MIN_THUMB_HEIGHT);
1090 let row_height = (height - thumb_height) / snapshot.max_point().row().as_f32();
1091
1092 Some(ScrollbarLayout {
1093 hitbox: cx.insert_hitbox(track_bounds, false),
1094 visible_row_range,
1095 row_height,
1096 visible: show_scrollbars,
1097 thumb_height,
1098 })
1099 }
1100
1101 #[allow(clippy::too_many_arguments)]
1102 fn prepaint_gutter_fold_toggles(
1103 &self,
1104 toggles: &mut [Option<AnyElement>],
1105 line_height: Pixels,
1106 gutter_dimensions: &GutterDimensions,
1107 gutter_settings: crate::editor_settings::Gutter,
1108 scroll_pixel_position: gpui::Point<Pixels>,
1109 gutter_hitbox: &Hitbox,
1110 cx: &mut WindowContext,
1111 ) {
1112 for (ix, fold_indicator) in toggles.iter_mut().enumerate() {
1113 if let Some(fold_indicator) = fold_indicator {
1114 debug_assert!(gutter_settings.folds);
1115 let available_space = size(
1116 AvailableSpace::MinContent,
1117 AvailableSpace::Definite(line_height * 0.55),
1118 );
1119 let fold_indicator_size = fold_indicator.layout_as_root(available_space, cx);
1120
1121 let position = point(
1122 gutter_dimensions.width - gutter_dimensions.right_padding,
1123 ix as f32 * line_height - (scroll_pixel_position.y % line_height),
1124 );
1125 let centering_offset = point(
1126 (gutter_dimensions.right_padding + gutter_dimensions.margin
1127 - fold_indicator_size.width)
1128 / 2.,
1129 (line_height - fold_indicator_size.height) / 2.,
1130 );
1131 let origin = gutter_hitbox.origin + position + centering_offset;
1132 fold_indicator.prepaint_as_root(origin, available_space, cx);
1133 }
1134 }
1135 }
1136
1137 #[allow(clippy::too_many_arguments)]
1138 fn prepaint_flap_trailers(
1139 &self,
1140 trailers: Vec<Option<AnyElement>>,
1141 lines: &[LineWithInvisibles],
1142 line_height: Pixels,
1143 content_origin: gpui::Point<Pixels>,
1144 scroll_pixel_position: gpui::Point<Pixels>,
1145 em_width: Pixels,
1146 cx: &mut WindowContext,
1147 ) -> Vec<Option<FlapTrailerLayout>> {
1148 trailers
1149 .into_iter()
1150 .enumerate()
1151 .map(|(ix, element)| {
1152 let mut element = element?;
1153 let available_space = size(
1154 AvailableSpace::MinContent,
1155 AvailableSpace::Definite(line_height),
1156 );
1157 let size = element.layout_as_root(available_space, cx);
1158
1159 let line = &lines[ix];
1160 let padding = if line.width == Pixels::ZERO {
1161 Pixels::ZERO
1162 } else {
1163 4. * em_width
1164 };
1165 let position = point(
1166 scroll_pixel_position.x + line.width + padding,
1167 ix as f32 * line_height - (scroll_pixel_position.y % line_height),
1168 );
1169 let centering_offset = point(px(0.), (line_height - size.height) / 2.);
1170 let origin = content_origin + position + centering_offset;
1171 element.prepaint_as_root(origin, available_space, cx);
1172 Some(FlapTrailerLayout {
1173 element,
1174 bounds: Bounds::new(origin, size),
1175 })
1176 })
1177 .collect()
1178 }
1179
1180 // Folds contained in a hunk are ignored apart from shrinking visual size
1181 // If a fold contains any hunks then that fold line is marked as modified
1182 fn layout_git_gutters(
1183 &self,
1184 line_height: Pixels,
1185 gutter_hitbox: &Hitbox,
1186 display_rows: Range<DisplayRow>,
1187 snapshot: &EditorSnapshot,
1188 cx: &mut WindowContext,
1189 ) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
1190 let buffer_snapshot = &snapshot.buffer_snapshot;
1191
1192 let buffer_start_row = MultiBufferRow(
1193 DisplayPoint::new(display_rows.start, 0)
1194 .to_point(snapshot)
1195 .row,
1196 );
1197 let buffer_end_row = MultiBufferRow(
1198 DisplayPoint::new(display_rows.end, 0)
1199 .to_point(snapshot)
1200 .row,
1201 );
1202
1203 let expanded_hunk_display_rows = self.editor.update(cx, |editor, _| {
1204 editor
1205 .expanded_hunks
1206 .hunks(false)
1207 .map(|expanded_hunk| {
1208 let start_row = expanded_hunk
1209 .hunk_range
1210 .start
1211 .to_display_point(snapshot)
1212 .row();
1213 let end_row = expanded_hunk
1214 .hunk_range
1215 .end
1216 .to_display_point(snapshot)
1217 .row();
1218 (start_row, end_row)
1219 })
1220 .collect::<HashMap<_, _>>()
1221 });
1222
1223 buffer_snapshot
1224 .git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
1225 .map(|hunk| diff_hunk_to_display(&hunk, snapshot))
1226 .dedup()
1227 .map(|hunk| {
1228 let hitbox = if let DisplayDiffHunk::Unfolded {
1229 display_row_range, ..
1230 } = &hunk
1231 {
1232 let was_expanded = expanded_hunk_display_rows
1233 .get(&display_row_range.start)
1234 .map(|expanded_end_row| expanded_end_row == &display_row_range.end)
1235 .unwrap_or(false);
1236 if was_expanded {
1237 None
1238 } else {
1239 let hunk_bounds = Self::diff_hunk_bounds(
1240 &snapshot,
1241 line_height,
1242 gutter_hitbox.bounds,
1243 &hunk,
1244 );
1245 Some(cx.insert_hitbox(hunk_bounds, true))
1246 }
1247 } else {
1248 None
1249 };
1250 (hunk, hitbox)
1251 })
1252 .collect()
1253 }
1254
1255 #[allow(clippy::too_many_arguments)]
1256 fn layout_inline_blame(
1257 &self,
1258 display_row: DisplayRow,
1259 display_snapshot: &DisplaySnapshot,
1260 line_layout: &LineWithInvisibles,
1261 flap_trailer: Option<&FlapTrailerLayout>,
1262 em_width: Pixels,
1263 content_origin: gpui::Point<Pixels>,
1264 scroll_pixel_position: gpui::Point<Pixels>,
1265 line_height: Pixels,
1266 cx: &mut WindowContext,
1267 ) -> Option<AnyElement> {
1268 if !self
1269 .editor
1270 .update(cx, |editor, cx| editor.render_git_blame_inline(cx))
1271 {
1272 return None;
1273 }
1274
1275 let workspace = self
1276 .editor
1277 .read(cx)
1278 .workspace
1279 .as_ref()
1280 .map(|(w, _)| w.clone());
1281
1282 let display_point = DisplayPoint::new(display_row, 0);
1283 let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
1284
1285 let blame = self.editor.read(cx).blame.clone()?;
1286 let blame_entry = blame
1287 .update(cx, |blame, cx| {
1288 blame.blame_for_rows([Some(buffer_row)], cx).next()
1289 })
1290 .flatten()?;
1291
1292 let mut element =
1293 render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
1294
1295 let start_y = content_origin.y
1296 + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
1297
1298 let start_x = {
1299 const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
1300
1301 let line_end = if let Some(flap_trailer) = flap_trailer {
1302 flap_trailer.bounds.right()
1303 } else {
1304 content_origin.x - scroll_pixel_position.x + line_layout.width
1305 };
1306 let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
1307
1308 let min_column_in_pixels = ProjectSettings::get_global(cx)
1309 .git
1310 .inline_blame
1311 .and_then(|settings| settings.min_column)
1312 .map(|col| self.column_pixels(col as usize, cx))
1313 .unwrap_or(px(0.));
1314 let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
1315
1316 cmp::max(padded_line_end, min_start)
1317 };
1318
1319 let absolute_offset = point(start_x, start_y);
1320 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
1321
1322 element.prepaint_as_root(absolute_offset, available_space, cx);
1323
1324 Some(element)
1325 }
1326
1327 #[allow(clippy::too_many_arguments)]
1328 fn layout_blame_entries(
1329 &self,
1330 buffer_rows: impl Iterator<Item = Option<MultiBufferRow>>,
1331 em_width: Pixels,
1332 scroll_position: gpui::Point<f32>,
1333 line_height: Pixels,
1334 gutter_hitbox: &Hitbox,
1335 max_width: Option<Pixels>,
1336 cx: &mut WindowContext,
1337 ) -> Option<Vec<AnyElement>> {
1338 if !self
1339 .editor
1340 .update(cx, |editor, cx| editor.render_git_blame_gutter(cx))
1341 {
1342 return None;
1343 }
1344
1345 let blame = self.editor.read(cx).blame.clone()?;
1346 let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
1347 blame.blame_for_rows(buffer_rows, cx).collect()
1348 });
1349
1350 let width = if let Some(max_width) = max_width {
1351 AvailableSpace::Definite(max_width)
1352 } else {
1353 AvailableSpace::MaxContent
1354 };
1355 let scroll_top = scroll_position.y * line_height;
1356 let start_x = em_width * 1;
1357
1358 let mut last_used_color: Option<(PlayerColor, Oid)> = None;
1359
1360 let shaped_lines = blamed_rows
1361 .into_iter()
1362 .enumerate()
1363 .flat_map(|(ix, blame_entry)| {
1364 if let Some(blame_entry) = blame_entry {
1365 let mut element = render_blame_entry(
1366 ix,
1367 &blame,
1368 blame_entry,
1369 &self.style,
1370 &mut last_used_color,
1371 self.editor.clone(),
1372 cx,
1373 );
1374
1375 let start_y = ix as f32 * line_height - (scroll_top % line_height);
1376 let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
1377
1378 element.prepaint_as_root(
1379 absolute_offset,
1380 size(width, AvailableSpace::MinContent),
1381 cx,
1382 );
1383
1384 Some(element)
1385 } else {
1386 None
1387 }
1388 })
1389 .collect();
1390
1391 Some(shaped_lines)
1392 }
1393
1394 #[allow(clippy::too_many_arguments)]
1395 fn layout_indent_guides(
1396 &self,
1397 content_origin: gpui::Point<Pixels>,
1398 text_origin: gpui::Point<Pixels>,
1399 visible_buffer_range: Range<MultiBufferRow>,
1400 scroll_pixel_position: gpui::Point<Pixels>,
1401 line_height: Pixels,
1402 snapshot: &DisplaySnapshot,
1403 cx: &mut WindowContext,
1404 ) -> Option<Vec<IndentGuideLayout>> {
1405 let indent_guides = self.editor.update(cx, |editor, cx| {
1406 editor.indent_guides(visible_buffer_range, snapshot, cx)
1407 })?;
1408
1409 let active_indent_guide_indices = self.editor.update(cx, |editor, cx| {
1410 editor
1411 .find_active_indent_guide_indices(&indent_guides, snapshot, cx)
1412 .unwrap_or_default()
1413 });
1414
1415 Some(
1416 indent_guides
1417 .into_iter()
1418 .enumerate()
1419 .filter_map(|(i, indent_guide)| {
1420 let single_indent_width =
1421 self.column_pixels(indent_guide.tab_size as usize, cx);
1422 let total_width = single_indent_width * indent_guide.depth as f32;
1423 let start_x = content_origin.x + total_width - scroll_pixel_position.x;
1424 if start_x >= text_origin.x {
1425 let (offset_y, length) = Self::calculate_indent_guide_bounds(
1426 indent_guide.multibuffer_row_range.clone(),
1427 line_height,
1428 snapshot,
1429 );
1430
1431 let start_y = content_origin.y + offset_y - scroll_pixel_position.y;
1432
1433 Some(IndentGuideLayout {
1434 origin: point(start_x, start_y),
1435 length,
1436 single_indent_width,
1437 depth: indent_guide.depth,
1438 active: active_indent_guide_indices.contains(&i),
1439 })
1440 } else {
1441 None
1442 }
1443 })
1444 .collect(),
1445 )
1446 }
1447
1448 fn calculate_indent_guide_bounds(
1449 row_range: Range<MultiBufferRow>,
1450 line_height: Pixels,
1451 snapshot: &DisplaySnapshot,
1452 ) -> (gpui::Pixels, gpui::Pixels) {
1453 let start_point = Point::new(row_range.start.0, 0);
1454 let end_point = Point::new(row_range.end.0, 0);
1455
1456 let row_range = start_point.to_display_point(snapshot).row()
1457 ..end_point.to_display_point(snapshot).row();
1458
1459 let mut prev_line = start_point;
1460 prev_line.row = prev_line.row.saturating_sub(1);
1461 let prev_line = prev_line.to_display_point(snapshot).row();
1462
1463 let mut cons_line = end_point;
1464 cons_line.row += 1;
1465 let cons_line = cons_line.to_display_point(snapshot).row();
1466
1467 let mut offset_y = row_range.start.0 as f32 * line_height;
1468 let mut length = (cons_line.0.saturating_sub(row_range.start.0)) as f32 * line_height;
1469
1470 // If we are at the end of the buffer, ensure that the indent guide extends to the end of the line.
1471 if row_range.end == cons_line {
1472 length += line_height;
1473 }
1474
1475 // If there is a block (e.g. diagnostic) in between the start of the indent guide and the line above,
1476 // we want to extend the indent guide to the start of the block.
1477 let mut block_height = 0;
1478 let mut block_offset = 0;
1479 let mut found_excerpt_header = false;
1480 for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
1481 if matches!(block, TransformBlock::ExcerptHeader { .. }) {
1482 found_excerpt_header = true;
1483 break;
1484 }
1485 block_offset += block.height();
1486 block_height += block.height();
1487 }
1488 if !found_excerpt_header {
1489 offset_y -= block_offset as f32 * line_height;
1490 length += block_height as f32 * line_height;
1491 }
1492
1493 // If there is a block (e.g. diagnostic) at the end of an multibuffer excerpt,
1494 // we want to ensure that the indent guide stops before the excerpt header.
1495 let mut block_height = 0;
1496 let mut found_excerpt_header = false;
1497 for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
1498 if matches!(block, TransformBlock::ExcerptHeader { .. }) {
1499 found_excerpt_header = true;
1500 }
1501 block_height += block.height();
1502 }
1503 if found_excerpt_header {
1504 length -= block_height as f32 * line_height;
1505 }
1506
1507 (offset_y, length)
1508 }
1509
1510 fn layout_run_indicators(
1511 &self,
1512 line_height: Pixels,
1513 scroll_pixel_position: gpui::Point<Pixels>,
1514 gutter_dimensions: &GutterDimensions,
1515 gutter_hitbox: &Hitbox,
1516 snapshot: &EditorSnapshot,
1517 cx: &mut WindowContext,
1518 ) -> Vec<AnyElement> {
1519 self.editor.update(cx, |editor, cx| {
1520 let active_task_indicator_row =
1521 if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
1522 deployed_from_indicator,
1523 actions,
1524 ..
1525 })) = editor.context_menu.read().as_ref()
1526 {
1527 actions
1528 .tasks
1529 .as_ref()
1530 .map(|tasks| tasks.position.to_display_point(snapshot).row())
1531 .or_else(|| *deployed_from_indicator)
1532 } else {
1533 None
1534 };
1535 editor
1536 .tasks
1537 .iter()
1538 .filter_map(|(_, tasks)| {
1539 let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
1540 let multibuffer_row = MultiBufferRow(multibuffer_point.row);
1541 if snapshot.is_line_folded(multibuffer_row) {
1542 return None;
1543 }
1544 let display_row = multibuffer_point.to_display_point(snapshot).row();
1545 let button = editor.render_run_indicator(
1546 &self.style,
1547 Some(display_row) == active_task_indicator_row,
1548 display_row,
1549 cx,
1550 );
1551
1552 let button = prepaint_gutter_button(
1553 button,
1554 display_row,
1555 line_height,
1556 gutter_dimensions,
1557 scroll_pixel_position,
1558 gutter_hitbox,
1559 cx,
1560 );
1561 Some(button)
1562 })
1563 .collect_vec()
1564 })
1565 }
1566
1567 fn layout_code_actions_indicator(
1568 &self,
1569 line_height: Pixels,
1570 newest_selection_head: DisplayPoint,
1571 scroll_pixel_position: gpui::Point<Pixels>,
1572 gutter_dimensions: &GutterDimensions,
1573 gutter_hitbox: &Hitbox,
1574 cx: &mut WindowContext,
1575 ) -> Option<AnyElement> {
1576 let mut active = false;
1577 let mut button = None;
1578 let row = newest_selection_head.row();
1579 self.editor.update(cx, |editor, cx| {
1580 if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
1581 deployed_from_indicator,
1582 ..
1583 })) = editor.context_menu.read().as_ref()
1584 {
1585 active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
1586 };
1587 button = editor.render_code_actions_indicator(&self.style, row, active, cx);
1588 });
1589
1590 let button = prepaint_gutter_button(
1591 button?,
1592 row,
1593 line_height,
1594 gutter_dimensions,
1595 scroll_pixel_position,
1596 gutter_hitbox,
1597 cx,
1598 );
1599
1600 Some(button)
1601 }
1602
1603 fn get_participant_color(
1604 participant_index: Option<ParticipantIndex>,
1605 cx: &WindowContext,
1606 ) -> PlayerColor {
1607 if let Some(index) = participant_index {
1608 cx.theme().players().color_for_participant(index.0)
1609 } else {
1610 cx.theme().players().absent()
1611 }
1612 }
1613
1614 fn calculate_relative_line_numbers(
1615 &self,
1616 snapshot: &EditorSnapshot,
1617 rows: &Range<DisplayRow>,
1618 relative_to: Option<DisplayRow>,
1619 ) -> HashMap<DisplayRow, DisplayRowDelta> {
1620 let mut relative_rows: HashMap<DisplayRow, DisplayRowDelta> = Default::default();
1621 let Some(relative_to) = relative_to else {
1622 return relative_rows;
1623 };
1624
1625 let start = rows.start.min(relative_to);
1626 let end = rows.end.max(relative_to);
1627
1628 let buffer_rows = snapshot
1629 .buffer_rows(start)
1630 .take(1 + end.minus(start) as usize)
1631 .collect::<Vec<_>>();
1632
1633 let head_idx = relative_to.minus(start);
1634 let mut delta = 1;
1635 let mut i = head_idx + 1;
1636 while i < buffer_rows.len() as u32 {
1637 if buffer_rows[i as usize].is_some() {
1638 if rows.contains(&DisplayRow(i + start.0)) {
1639 relative_rows.insert(DisplayRow(i + start.0), delta);
1640 }
1641 delta += 1;
1642 }
1643 i += 1;
1644 }
1645 delta = 1;
1646 i = head_idx.min(buffer_rows.len() as u32 - 1);
1647 while i > 0 && buffer_rows[i as usize].is_none() {
1648 i -= 1;
1649 }
1650
1651 while i > 0 {
1652 i -= 1;
1653 if buffer_rows[i as usize].is_some() {
1654 if rows.contains(&DisplayRow(i + start.0)) {
1655 relative_rows.insert(DisplayRow(i + start.0), delta);
1656 }
1657 delta += 1;
1658 }
1659 }
1660
1661 relative_rows
1662 }
1663
1664 fn layout_line_numbers(
1665 &self,
1666 rows: Range<DisplayRow>,
1667 buffer_rows: impl Iterator<Item = Option<MultiBufferRow>>,
1668 active_rows: &BTreeMap<DisplayRow, bool>,
1669 newest_selection_head: Option<DisplayPoint>,
1670 snapshot: &EditorSnapshot,
1671 cx: &mut WindowContext,
1672 ) -> Vec<Option<ShapedLine>> {
1673 let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
1674 EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full
1675 });
1676 if !include_line_numbers {
1677 return Vec::new();
1678 }
1679
1680 let editor = self.editor.read(cx);
1681 let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
1682 let newest = editor.selections.newest::<Point>(cx);
1683 SelectionLayout::new(
1684 newest,
1685 editor.selections.line_mode,
1686 editor.cursor_shape,
1687 &snapshot.display_snapshot,
1688 true,
1689 true,
1690 None,
1691 )
1692 .head
1693 });
1694 let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
1695
1696 let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
1697 let relative_to = if is_relative {
1698 Some(newest_selection_head.row())
1699 } else {
1700 None
1701 };
1702 let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
1703 let mut line_number = String::new();
1704 buffer_rows
1705 .into_iter()
1706 .enumerate()
1707 .map(|(ix, multibuffer_row)| {
1708 let multibuffer_row = multibuffer_row?;
1709 let display_row = DisplayRow(rows.start.0 + ix as u32);
1710 let color = if active_rows.contains_key(&display_row) {
1711 cx.theme().colors().editor_active_line_number
1712 } else {
1713 cx.theme().colors().editor_line_number
1714 };
1715 line_number.clear();
1716 let default_number = multibuffer_row.0 + 1;
1717 let number = relative_rows
1718 .get(&DisplayRow(ix as u32 + rows.start.0))
1719 .unwrap_or(&default_number);
1720 write!(&mut line_number, "{number}").unwrap();
1721 let run = TextRun {
1722 len: line_number.len(),
1723 font: self.style.text.font(),
1724 color,
1725 background_color: None,
1726 underline: None,
1727 strikethrough: None,
1728 };
1729 let shaped_line = cx
1730 .text_system()
1731 .shape_line(line_number.clone().into(), font_size, &[run])
1732 .unwrap();
1733 Some(shaped_line)
1734 })
1735 .collect()
1736 }
1737
1738 fn layout_gutter_fold_toggles(
1739 &self,
1740 rows: Range<DisplayRow>,
1741 buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
1742 active_rows: &BTreeMap<DisplayRow, bool>,
1743 snapshot: &EditorSnapshot,
1744 cx: &mut WindowContext,
1745 ) -> Vec<Option<AnyElement>> {
1746 let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
1747 && snapshot.mode == EditorMode::Full
1748 && self.editor.read(cx).is_singleton(cx);
1749 if include_fold_statuses {
1750 buffer_rows
1751 .into_iter()
1752 .enumerate()
1753 .map(|(ix, row)| {
1754 if let Some(multibuffer_row) = row {
1755 let display_row = DisplayRow(rows.start.0 + ix as u32);
1756 let active = active_rows.contains_key(&display_row);
1757 snapshot.render_fold_toggle(
1758 multibuffer_row,
1759 active,
1760 self.editor.clone(),
1761 cx,
1762 )
1763 } else {
1764 None
1765 }
1766 })
1767 .collect()
1768 } else {
1769 Vec::new()
1770 }
1771 }
1772
1773 fn layout_flap_trailers(
1774 &self,
1775 buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
1776 snapshot: &EditorSnapshot,
1777 cx: &mut WindowContext,
1778 ) -> Vec<Option<AnyElement>> {
1779 buffer_rows
1780 .into_iter()
1781 .map(|row| {
1782 if let Some(multibuffer_row) = row {
1783 snapshot.render_flap_trailer(multibuffer_row, cx)
1784 } else {
1785 None
1786 }
1787 })
1788 .collect()
1789 }
1790
1791 fn layout_lines(
1792 &self,
1793 rows: Range<DisplayRow>,
1794 line_number_layouts: &[Option<ShapedLine>],
1795 snapshot: &EditorSnapshot,
1796 cx: &mut WindowContext,
1797 ) -> Vec<LineWithInvisibles> {
1798 if rows.start >= rows.end {
1799 return Vec::new();
1800 }
1801
1802 // Show the placeholder when the editor is empty
1803 if snapshot.is_empty() {
1804 let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
1805 let placeholder_color = cx.theme().colors().text_placeholder;
1806 let placeholder_text = snapshot.placeholder_text();
1807
1808 let placeholder_lines = placeholder_text
1809 .as_ref()
1810 .map_or("", AsRef::as_ref)
1811 .split('\n')
1812 .skip(rows.start.0 as usize)
1813 .chain(iter::repeat(""))
1814 .take(rows.len());
1815 placeholder_lines
1816 .filter_map(move |line| {
1817 let run = TextRun {
1818 len: line.len(),
1819 font: self.style.text.font(),
1820 color: placeholder_color,
1821 background_color: None,
1822 underline: Default::default(),
1823 strikethrough: None,
1824 };
1825 cx.text_system()
1826 .shape_line(line.to_string().into(), font_size, &[run])
1827 .log_err()
1828 })
1829 .map(|line| LineWithInvisibles {
1830 width: line.width,
1831 len: line.len,
1832 fragments: smallvec![LineFragment::Text(line)],
1833 invisibles: Vec::new(),
1834 font_size,
1835 })
1836 .collect()
1837 } else {
1838 let chunks = snapshot.highlighted_chunks(rows.clone(), true, &self.style);
1839 LineWithInvisibles::from_chunks(
1840 chunks,
1841 &self.style.text,
1842 MAX_LINE_LEN,
1843 rows.len(),
1844 line_number_layouts,
1845 snapshot.mode,
1846 cx,
1847 )
1848 }
1849 }
1850
1851 fn prepaint_lines(
1852 &self,
1853 start_row: DisplayRow,
1854 line_layouts: &mut [LineWithInvisibles],
1855 line_height: Pixels,
1856 scroll_pixel_position: gpui::Point<Pixels>,
1857 content_origin: gpui::Point<Pixels>,
1858 cx: &mut WindowContext,
1859 ) -> SmallVec<[AnyElement; 1]> {
1860 let mut line_elements = SmallVec::new();
1861 for (ix, line) in line_layouts.iter_mut().enumerate() {
1862 let row = start_row + DisplayRow(ix as u32);
1863 line.prepaint(
1864 line_height,
1865 scroll_pixel_position,
1866 row,
1867 content_origin,
1868 &mut line_elements,
1869 cx,
1870 );
1871 }
1872 line_elements
1873 }
1874
1875 #[allow(clippy::too_many_arguments)]
1876 fn build_blocks(
1877 &self,
1878 rows: Range<DisplayRow>,
1879 snapshot: &EditorSnapshot,
1880 hitbox: &Hitbox,
1881 text_hitbox: &Hitbox,
1882 scroll_width: &mut Pixels,
1883 gutter_dimensions: &GutterDimensions,
1884 em_width: Pixels,
1885 text_x: Pixels,
1886 line_height: Pixels,
1887 line_layouts: &[LineWithInvisibles],
1888 cx: &mut WindowContext,
1889 ) -> Vec<BlockLayout> {
1890 let mut block_id = 0;
1891 let (fixed_blocks, non_fixed_blocks) = snapshot
1892 .blocks_in_range(rows.clone())
1893 .partition::<Vec<_>, _>(|(_, block)| match block {
1894 TransformBlock::ExcerptHeader { .. } => false,
1895 TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
1896 });
1897
1898 let render_block = |block: &TransformBlock,
1899 available_space: Size<AvailableSpace>,
1900 block_id: usize,
1901 block_row_start: DisplayRow,
1902 cx: &mut WindowContext| {
1903 let mut element = match block {
1904 TransformBlock::Custom(block) => {
1905 let align_to = block
1906 .position()
1907 .to_point(&snapshot.buffer_snapshot)
1908 .to_display_point(snapshot);
1909 let anchor_x = text_x
1910 + if rows.contains(&align_to.row()) {
1911 line_layouts[align_to.row().minus(rows.start) as usize]
1912 .x_for_index(align_to.column() as usize)
1913 } else {
1914 layout_line(align_to.row(), snapshot, &self.style, cx)
1915 .x_for_index(align_to.column() as usize)
1916 };
1917
1918 block.render(&mut BlockContext {
1919 context: cx,
1920 anchor_x,
1921 gutter_dimensions,
1922 line_height,
1923 em_width,
1924 block_id,
1925 max_width: text_hitbox.size.width.max(*scroll_width),
1926 editor_style: &self.style,
1927 })
1928 }
1929
1930 TransformBlock::ExcerptHeader {
1931 buffer,
1932 range,
1933 starts_new_buffer,
1934 height,
1935 id,
1936 ..
1937 } => {
1938 let include_root = self
1939 .editor
1940 .read(cx)
1941 .project
1942 .as_ref()
1943 .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
1944 .unwrap_or_default();
1945
1946 #[derive(Clone)]
1947 struct JumpData {
1948 position: Point,
1949 anchor: text::Anchor,
1950 path: ProjectPath,
1951 line_offset_from_top: u32,
1952 }
1953
1954 let jump_data = project::File::from_dyn(buffer.file()).map(|file| {
1955 let jump_path = ProjectPath {
1956 worktree_id: file.worktree_id(cx),
1957 path: file.path.clone(),
1958 };
1959 let jump_anchor = range
1960 .primary
1961 .as_ref()
1962 .map_or(range.context.start, |primary| primary.start);
1963
1964 let excerpt_start = range.context.start;
1965 let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
1966 let offset_from_excerpt_start = if jump_anchor == excerpt_start {
1967 0
1968 } else {
1969 let excerpt_start_row =
1970 language::ToPoint::to_point(&jump_anchor, buffer).row;
1971 jump_position.row - excerpt_start_row
1972 };
1973
1974 let line_offset_from_top =
1975 block_row_start.0 + *height as u32 + offset_from_excerpt_start
1976 - snapshot
1977 .scroll_anchor
1978 .scroll_position(&snapshot.display_snapshot)
1979 .y as u32;
1980
1981 JumpData {
1982 position: jump_position,
1983 anchor: jump_anchor,
1984 path: jump_path,
1985 line_offset_from_top,
1986 }
1987 });
1988
1989 let element = if *starts_new_buffer {
1990 let path = buffer.resolve_file_path(cx, include_root);
1991 let mut filename = None;
1992 let mut parent_path = None;
1993 // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
1994 if let Some(path) = path {
1995 filename = path.file_name().map(|f| f.to_string_lossy().to_string());
1996 parent_path = path
1997 .parent()
1998 .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
1999 }
2000
2001 v_flex()
2002 .id(("path header container", block_id))
2003 .size_full()
2004 .justify_center()
2005 .p(gpui::px(6.))
2006 .child(
2007 h_flex()
2008 .id("path header block")
2009 .size_full()
2010 .pl(gpui::px(12.))
2011 .pr(gpui::px(8.))
2012 .rounded_md()
2013 .shadow_md()
2014 .border_1()
2015 .border_color(cx.theme().colors().border)
2016 .bg(cx.theme().colors().editor_subheader_background)
2017 .justify_between()
2018 .hover(|style| style.bg(cx.theme().colors().element_hover))
2019 .child(
2020 h_flex().gap_3().child(
2021 h_flex()
2022 .gap_2()
2023 .child(
2024 filename
2025 .map(SharedString::from)
2026 .unwrap_or_else(|| "untitled".into()),
2027 )
2028 .when_some(parent_path, |then, path| {
2029 then.child(
2030 div().child(path).text_color(
2031 cx.theme().colors().text_muted,
2032 ),
2033 )
2034 }),
2035 ),
2036 )
2037 .when_some(jump_data.clone(), |this, jump_data| {
2038 this.cursor_pointer()
2039 .tooltip(|cx| {
2040 Tooltip::for_action(
2041 "Jump to File",
2042 &OpenExcerpts,
2043 cx,
2044 )
2045 })
2046 .on_mouse_down(MouseButton::Left, |_, cx| {
2047 cx.stop_propagation()
2048 })
2049 .on_click(cx.listener_for(&self.editor, {
2050 move |editor, _, cx| {
2051 editor.jump(
2052 jump_data.path.clone(),
2053 jump_data.position,
2054 jump_data.anchor,
2055 jump_data.line_offset_from_top,
2056 cx,
2057 );
2058 }
2059 }))
2060 }),
2061 )
2062 } else {
2063 v_flex()
2064 .id(("collapsed context", block_id))
2065 .size_full()
2066 .child(
2067 div()
2068 .flex()
2069 .v_flex()
2070 .justify_start()
2071 .id("jump to collapsed context")
2072 .w(relative(1.0))
2073 .h_full()
2074 .child(
2075 div()
2076 .h_px()
2077 .w_full()
2078 .bg(cx.theme().colors().border_variant)
2079 .group_hover("excerpt-jump-action", |style| {
2080 style.bg(cx.theme().colors().border)
2081 }),
2082 ),
2083 )
2084 .child(
2085 h_flex()
2086 .justify_end()
2087 .flex_none()
2088 .w(
2089 gutter_dimensions.width - (gutter_dimensions.left_padding), // + gutter_dimensions.right_padding)
2090 )
2091 .h_full()
2092 .child(
2093 ButtonLike::new("expand-icon")
2094 .style(ButtonStyle::Transparent)
2095 .child(
2096 svg()
2097 .path(IconName::ExpandVertical.path())
2098 .size(IconSize::XSmall.rems())
2099 .text_color(
2100 cx.theme().colors().editor_line_number,
2101 )
2102 .group("")
2103 .hover(|style| {
2104 style.text_color(
2105 cx.theme()
2106 .colors()
2107 .editor_active_line_number,
2108 )
2109 }),
2110 )
2111 .on_click(cx.listener_for(&self.editor, {
2112 let id = *id;
2113 move |editor, _, cx| {
2114 editor.expand_excerpt(id, cx);
2115 }
2116 }))
2117 .tooltip({
2118 move |cx| {
2119 Tooltip::for_action(
2120 "Expand Excerpt",
2121 &ExpandExcerpts { lines: 0 },
2122 cx,
2123 )
2124 }
2125 }),
2126 ),
2127 )
2128 .group("excerpt-jump-action")
2129 .cursor_pointer()
2130 .when_some(jump_data.clone(), |this, jump_data| {
2131 this.on_click(cx.listener_for(&self.editor, {
2132 let path = jump_data.path.clone();
2133 move |editor, _, cx| {
2134 cx.stop_propagation();
2135
2136 editor.jump(
2137 path.clone(),
2138 jump_data.position,
2139 jump_data.anchor,
2140 jump_data.line_offset_from_top,
2141 cx,
2142 );
2143 }
2144 }))
2145 .tooltip(move |cx| {
2146 Tooltip::for_action(
2147 format!(
2148 "Jump to {}:L{}",
2149 jump_data.path.path.display(),
2150 jump_data.position.row + 1
2151 ),
2152 &OpenExcerpts,
2153 cx,
2154 )
2155 })
2156 })
2157 };
2158 element.into_any()
2159 }
2160 };
2161
2162 let size = element.layout_as_root(available_space, cx);
2163 (element, size)
2164 };
2165
2166 let mut fixed_block_max_width = Pixels::ZERO;
2167 let mut blocks = Vec::new();
2168 for (row, block) in fixed_blocks {
2169 let available_space = size(
2170 AvailableSpace::MinContent,
2171 AvailableSpace::Definite(block.height() as f32 * line_height),
2172 );
2173 let (element, element_size) = render_block(block, available_space, block_id, row, cx);
2174 block_id += 1;
2175 fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
2176 blocks.push(BlockLayout {
2177 row,
2178 element,
2179 available_space,
2180 style: BlockStyle::Fixed,
2181 });
2182 }
2183 for (row, block) in non_fixed_blocks {
2184 let style = match block {
2185 TransformBlock::Custom(block) => block.style(),
2186 TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky,
2187 };
2188 let width = match style {
2189 BlockStyle::Sticky => hitbox.size.width,
2190 BlockStyle::Flex => hitbox
2191 .size
2192 .width
2193 .max(fixed_block_max_width)
2194 .max(gutter_dimensions.width + *scroll_width),
2195 BlockStyle::Fixed => unreachable!(),
2196 };
2197 let available_space = size(
2198 AvailableSpace::Definite(width),
2199 AvailableSpace::Definite(block.height() as f32 * line_height),
2200 );
2201 let (element, _) = render_block(block, available_space, block_id, row, cx);
2202 block_id += 1;
2203 blocks.push(BlockLayout {
2204 row,
2205 element,
2206 available_space,
2207 style,
2208 });
2209 }
2210
2211 *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width);
2212 blocks
2213 }
2214
2215 fn layout_blocks(
2216 &self,
2217 blocks: &mut Vec<BlockLayout>,
2218 hitbox: &Hitbox,
2219 line_height: Pixels,
2220 scroll_pixel_position: gpui::Point<Pixels>,
2221 cx: &mut WindowContext,
2222 ) {
2223 for block in blocks {
2224 let mut origin = hitbox.origin
2225 + point(
2226 Pixels::ZERO,
2227 block.row.as_f32() * line_height - scroll_pixel_position.y,
2228 );
2229 if !matches!(block.style, BlockStyle::Sticky) {
2230 origin += point(-scroll_pixel_position.x, Pixels::ZERO);
2231 }
2232 block
2233 .element
2234 .prepaint_as_root(origin, block.available_space, cx);
2235 }
2236 }
2237
2238 #[allow(clippy::too_many_arguments)]
2239 fn layout_context_menu(
2240 &self,
2241 line_height: Pixels,
2242 hitbox: &Hitbox,
2243 text_hitbox: &Hitbox,
2244 content_origin: gpui::Point<Pixels>,
2245 start_row: DisplayRow,
2246 scroll_pixel_position: gpui::Point<Pixels>,
2247 line_layouts: &[LineWithInvisibles],
2248 newest_selection_head: DisplayPoint,
2249 gutter_overshoot: Pixels,
2250 cx: &mut WindowContext,
2251 ) -> bool {
2252 let max_height = cmp::min(
2253 12. * line_height,
2254 cmp::max(3. * line_height, (hitbox.size.height - line_height) / 2.),
2255 );
2256 let Some((position, mut context_menu)) = self.editor.update(cx, |editor, cx| {
2257 if editor.context_menu_visible() {
2258 editor.render_context_menu(newest_selection_head, &self.style, max_height, cx)
2259 } else {
2260 None
2261 }
2262 }) else {
2263 return false;
2264 };
2265
2266 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
2267 let context_menu_size = context_menu.layout_as_root(available_space, cx);
2268
2269 let (x, y) = match position {
2270 crate::ContextMenuOrigin::EditorPoint(point) => {
2271 let cursor_row_layout = &line_layouts[point.row().minus(start_row) as usize];
2272 let x = cursor_row_layout.x_for_index(point.column() as usize)
2273 - scroll_pixel_position.x;
2274 let y = point.row().next_row().as_f32() * line_height - scroll_pixel_position.y;
2275 (x, y)
2276 }
2277 crate::ContextMenuOrigin::GutterIndicator(row) => {
2278 // Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the indicator than just a plain first column of the
2279 // text field.
2280 let x = -gutter_overshoot;
2281 let y = row.next_row().as_f32() * line_height - scroll_pixel_position.y;
2282 (x, y)
2283 }
2284 };
2285
2286 let mut list_origin = content_origin + point(x, y);
2287 let list_width = context_menu_size.width;
2288 let list_height = context_menu_size.height;
2289
2290 // Snap the right edge of the list to the right edge of the window if
2291 // its horizontal bounds overflow.
2292 if list_origin.x + list_width > cx.viewport_size().width {
2293 list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
2294 }
2295
2296 if list_origin.y + list_height > text_hitbox.lower_right().y {
2297 list_origin.y -= line_height + list_height;
2298 }
2299
2300 cx.defer_draw(context_menu, list_origin, 1);
2301 true
2302 }
2303
2304 fn layout_mouse_context_menu(&self, cx: &mut WindowContext) -> Option<AnyElement> {
2305 let mouse_context_menu = self.editor.read(cx).mouse_context_menu.as_ref()?;
2306 let mut element = deferred(
2307 anchored()
2308 .position(mouse_context_menu.position)
2309 .child(mouse_context_menu.context_menu.clone())
2310 .anchor(AnchorCorner::TopLeft)
2311 .snap_to_window(),
2312 )
2313 .with_priority(1)
2314 .into_any();
2315
2316 element.prepaint_as_root(gpui::Point::default(), AvailableSpace::min_size(), cx);
2317 Some(element)
2318 }
2319
2320 #[allow(clippy::too_many_arguments)]
2321 fn layout_hover_popovers(
2322 &self,
2323 snapshot: &EditorSnapshot,
2324 hitbox: &Hitbox,
2325 text_hitbox: &Hitbox,
2326 visible_display_row_range: Range<DisplayRow>,
2327 content_origin: gpui::Point<Pixels>,
2328 scroll_pixel_position: gpui::Point<Pixels>,
2329 line_layouts: &[LineWithInvisibles],
2330 line_height: Pixels,
2331 em_width: Pixels,
2332 cx: &mut WindowContext,
2333 ) {
2334 struct MeasuredHoverPopover {
2335 element: AnyElement,
2336 size: Size<Pixels>,
2337 horizontal_offset: Pixels,
2338 }
2339
2340 let max_size = size(
2341 (120. * em_width) // Default size
2342 .min(hitbox.size.width / 2.) // Shrink to half of the editor width
2343 .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
2344 (16. * line_height) // Default size
2345 .min(hitbox.size.height / 2.) // Shrink to half of the editor height
2346 .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
2347 );
2348
2349 let hover_popovers = self.editor.update(cx, |editor, cx| {
2350 editor.hover_state.render(
2351 &snapshot,
2352 &self.style,
2353 visible_display_row_range.clone(),
2354 max_size,
2355 editor.workspace.as_ref().map(|(w, _)| w.clone()),
2356 cx,
2357 )
2358 });
2359 let Some((position, hover_popovers)) = hover_popovers else {
2360 return;
2361 };
2362
2363 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
2364
2365 // This is safe because we check on layout whether the required row is available
2366 let hovered_row_layout =
2367 &line_layouts[position.row().minus(visible_display_row_range.start) as usize];
2368
2369 // Compute Hovered Point
2370 let x =
2371 hovered_row_layout.x_for_index(position.column() as usize) - scroll_pixel_position.x;
2372 let y = position.row().as_f32() * line_height - scroll_pixel_position.y;
2373 let hovered_point = content_origin + point(x, y);
2374
2375 let mut overall_height = Pixels::ZERO;
2376 let mut measured_hover_popovers = Vec::new();
2377 for mut hover_popover in hover_popovers {
2378 let size = hover_popover.layout_as_root(available_space, cx);
2379 let horizontal_offset =
2380 (text_hitbox.upper_right().x - (hovered_point.x + size.width)).min(Pixels::ZERO);
2381
2382 overall_height += HOVER_POPOVER_GAP + size.height;
2383
2384 measured_hover_popovers.push(MeasuredHoverPopover {
2385 element: hover_popover,
2386 size,
2387 horizontal_offset,
2388 });
2389 }
2390 overall_height += HOVER_POPOVER_GAP;
2391
2392 fn draw_occluder(width: Pixels, origin: gpui::Point<Pixels>, cx: &mut WindowContext) {
2393 let mut occlusion = div()
2394 .size_full()
2395 .occlude()
2396 .on_mouse_move(|_, cx| cx.stop_propagation())
2397 .into_any_element();
2398 occlusion.layout_as_root(size(width, HOVER_POPOVER_GAP).into(), cx);
2399 cx.defer_draw(occlusion, origin, 2);
2400 }
2401
2402 if hovered_point.y > overall_height {
2403 // There is enough space above. Render popovers above the hovered point
2404 let mut current_y = hovered_point.y;
2405 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
2406 let size = popover.size;
2407 let popover_origin = point(
2408 hovered_point.x + popover.horizontal_offset,
2409 current_y - size.height,
2410 );
2411
2412 cx.defer_draw(popover.element, popover_origin, 2);
2413 if position != itertools::Position::Last {
2414 let origin = point(popover_origin.x, popover_origin.y - HOVER_POPOVER_GAP);
2415 draw_occluder(size.width, origin, cx);
2416 }
2417
2418 current_y = popover_origin.y - HOVER_POPOVER_GAP;
2419 }
2420 } else {
2421 // There is not enough space above. Render popovers below the hovered point
2422 let mut current_y = hovered_point.y + line_height;
2423 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
2424 let size = popover.size;
2425 let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
2426
2427 cx.defer_draw(popover.element, popover_origin, 2);
2428 if position != itertools::Position::Last {
2429 let origin = point(popover_origin.x, popover_origin.y + size.height);
2430 draw_occluder(size.width, origin, cx);
2431 }
2432
2433 current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
2434 }
2435 }
2436 }
2437
2438 fn paint_background(&self, layout: &EditorLayout, cx: &mut WindowContext) {
2439 cx.paint_layer(layout.hitbox.bounds, |cx| {
2440 let scroll_top = layout.position_map.snapshot.scroll_position().y;
2441 let gutter_bg = cx.theme().colors().editor_gutter_background;
2442 cx.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg));
2443 cx.paint_quad(fill(layout.text_hitbox.bounds, self.style.background));
2444
2445 if let EditorMode::Full = layout.mode {
2446 let mut active_rows = layout.active_rows.iter().peekable();
2447 while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
2448 let mut end_row = start_row.0;
2449 while active_rows
2450 .peek()
2451 .map_or(false, |(active_row, has_selection)| {
2452 active_row.0 == end_row + 1
2453 && *has_selection == contains_non_empty_selection
2454 })
2455 {
2456 active_rows.next().unwrap();
2457 end_row += 1;
2458 }
2459
2460 if !contains_non_empty_selection {
2461 let highlight_h_range =
2462 match layout.position_map.snapshot.current_line_highlight {
2463 CurrentLineHighlight::Gutter => Some(Range {
2464 start: layout.hitbox.left(),
2465 end: layout.gutter_hitbox.right(),
2466 }),
2467 CurrentLineHighlight::Line => Some(Range {
2468 start: layout.text_hitbox.bounds.left(),
2469 end: layout.text_hitbox.bounds.right(),
2470 }),
2471 CurrentLineHighlight::All => Some(Range {
2472 start: layout.hitbox.left(),
2473 end: layout.hitbox.right(),
2474 }),
2475 CurrentLineHighlight::None => None,
2476 };
2477 if let Some(range) = highlight_h_range {
2478 let active_line_bg = cx.theme().colors().editor_active_line_background;
2479 let bounds = Bounds {
2480 origin: point(
2481 range.start,
2482 layout.hitbox.origin.y
2483 + (start_row.as_f32() - scroll_top)
2484 * layout.position_map.line_height,
2485 ),
2486 size: size(
2487 range.end - range.start,
2488 layout.position_map.line_height
2489 * (end_row - start_row.0 + 1) as f32,
2490 ),
2491 };
2492 cx.paint_quad(fill(bounds, active_line_bg));
2493 }
2494 }
2495 }
2496
2497 let mut paint_highlight =
2498 |highlight_row_start: DisplayRow, highlight_row_end: DisplayRow, color| {
2499 let origin = point(
2500 layout.hitbox.origin.x,
2501 layout.hitbox.origin.y
2502 + (highlight_row_start.as_f32() - scroll_top)
2503 * layout.position_map.line_height,
2504 );
2505 let size = size(
2506 layout.hitbox.size.width,
2507 layout.position_map.line_height
2508 * highlight_row_end.next_row().minus(highlight_row_start) as f32,
2509 );
2510 cx.paint_quad(fill(Bounds { origin, size }, color));
2511 };
2512
2513 let mut current_paint: Option<(Hsla, Range<DisplayRow>)> = None;
2514 for (&new_row, &new_color) in &layout.highlighted_rows {
2515 match &mut current_paint {
2516 Some((current_color, current_range)) => {
2517 let current_color = *current_color;
2518 let new_range_started = current_color != new_color
2519 || current_range.end.next_row() != new_row;
2520 if new_range_started {
2521 paint_highlight(
2522 current_range.start,
2523 current_range.end,
2524 current_color,
2525 );
2526 current_paint = Some((new_color, new_row..new_row));
2527 continue;
2528 } else {
2529 current_range.end = current_range.end.next_row();
2530 }
2531 }
2532 None => current_paint = Some((new_color, new_row..new_row)),
2533 };
2534 }
2535 if let Some((color, range)) = current_paint {
2536 paint_highlight(range.start, range.end, color);
2537 }
2538
2539 let scroll_left =
2540 layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width;
2541
2542 for (wrap_position, active) in layout.wrap_guides.iter() {
2543 let x = (layout.text_hitbox.origin.x
2544 + *wrap_position
2545 + layout.position_map.em_width / 2.)
2546 - scroll_left;
2547
2548 let show_scrollbars = layout
2549 .scrollbar_layout
2550 .as_ref()
2551 .map_or(false, |scrollbar| scrollbar.visible);
2552 if x < layout.text_hitbox.origin.x
2553 || (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds))
2554 {
2555 continue;
2556 }
2557
2558 let color = if *active {
2559 cx.theme().colors().editor_active_wrap_guide
2560 } else {
2561 cx.theme().colors().editor_wrap_guide
2562 };
2563 cx.paint_quad(fill(
2564 Bounds {
2565 origin: point(x, layout.text_hitbox.origin.y),
2566 size: size(px(1.), layout.text_hitbox.size.height),
2567 },
2568 color,
2569 ));
2570 }
2571 }
2572 })
2573 }
2574
2575 fn paint_indent_guides(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
2576 let Some(indent_guides) = &layout.indent_guides else {
2577 return;
2578 };
2579
2580 let settings = self
2581 .editor
2582 .read(cx)
2583 .buffer()
2584 .read(cx)
2585 .settings_at(0, cx)
2586 .indent_guides;
2587
2588 let faded_color = |color: Hsla, alpha: f32| {
2589 let mut faded = color;
2590 faded.a = alpha;
2591 faded
2592 };
2593
2594 for indent_guide in indent_guides {
2595 let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
2596
2597 // TODO fixed for now, expose them through themes later
2598 const INDENT_AWARE_ALPHA: f32 = 0.2;
2599 const INDENT_AWARE_ACTIVE_ALPHA: f32 = 0.4;
2600 const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
2601 const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
2602
2603 let line_color = match (&settings.coloring, indent_guide.active) {
2604 (IndentGuideColoring::Disabled, _) => None,
2605 (IndentGuideColoring::Fixed, false) => {
2606 Some(cx.theme().colors().editor_indent_guide)
2607 }
2608 (IndentGuideColoring::Fixed, true) => {
2609 Some(cx.theme().colors().editor_indent_guide_active)
2610 }
2611 (IndentGuideColoring::IndentAware, false) => {
2612 Some(faded_color(indent_accent_colors, INDENT_AWARE_ALPHA))
2613 }
2614 (IndentGuideColoring::IndentAware, true) => {
2615 Some(faded_color(indent_accent_colors, INDENT_AWARE_ACTIVE_ALPHA))
2616 }
2617 };
2618
2619 let background_color = match (&settings.background_coloring, indent_guide.active) {
2620 (IndentGuideBackgroundColoring::Disabled, _) => None,
2621 (IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
2622 indent_accent_colors,
2623 INDENT_AWARE_BACKGROUND_ALPHA,
2624 )),
2625 (IndentGuideBackgroundColoring::IndentAware, true) => Some(faded_color(
2626 indent_accent_colors,
2627 INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA,
2628 )),
2629 };
2630
2631 let requested_line_width = settings.line_width.clamp(1, 10);
2632 let mut line_indicator_width = 0.;
2633 if let Some(color) = line_color {
2634 cx.paint_quad(fill(
2635 Bounds {
2636 origin: indent_guide.origin,
2637 size: size(px(requested_line_width as f32), indent_guide.length),
2638 },
2639 color,
2640 ));
2641 line_indicator_width = requested_line_width as f32;
2642 }
2643
2644 if let Some(color) = background_color {
2645 let width = indent_guide.single_indent_width - px(line_indicator_width);
2646 cx.paint_quad(fill(
2647 Bounds {
2648 origin: point(
2649 indent_guide.origin.x + px(line_indicator_width),
2650 indent_guide.origin.y,
2651 ),
2652 size: size(width, indent_guide.length),
2653 },
2654 color,
2655 ));
2656 }
2657 }
2658 }
2659
2660 fn paint_gutter(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
2661 let line_height = layout.position_map.line_height;
2662
2663 let scroll_position = layout.position_map.snapshot.scroll_position();
2664 let scroll_top = scroll_position.y * line_height;
2665
2666 cx.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
2667 for (_, hunk_hitbox) in &layout.display_hunks {
2668 if let Some(hunk_hitbox) = hunk_hitbox {
2669 cx.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
2670 }
2671 }
2672
2673 let show_git_gutter = layout
2674 .position_map
2675 .snapshot
2676 .show_git_diff_gutter
2677 .unwrap_or_else(|| {
2678 matches!(
2679 ProjectSettings::get_global(cx).git.git_gutter,
2680 Some(GitGutterSetting::TrackedFiles)
2681 )
2682 });
2683 if show_git_gutter {
2684 Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
2685 }
2686
2687 if layout.blamed_display_rows.is_some() {
2688 self.paint_blamed_display_rows(layout, cx);
2689 }
2690
2691 for (ix, line) in layout.line_numbers.iter().enumerate() {
2692 if let Some(line) = line {
2693 let line_origin = layout.gutter_hitbox.origin
2694 + point(
2695 layout.gutter_hitbox.size.width
2696 - line.width
2697 - layout.gutter_dimensions.right_padding,
2698 ix as f32 * line_height - (scroll_top % line_height),
2699 );
2700
2701 line.paint(line_origin, line_height, cx).log_err();
2702 }
2703 }
2704
2705 cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
2706 cx.with_element_namespace("gutter_fold_toggles", |cx| {
2707 for fold_indicator in layout.gutter_fold_toggles.iter_mut().flatten() {
2708 fold_indicator.paint(cx);
2709 }
2710 });
2711
2712 for test_indicators in layout.test_indicators.iter_mut() {
2713 test_indicators.paint(cx);
2714 }
2715
2716 if let Some(indicator) = layout.code_actions_indicator.as_mut() {
2717 indicator.paint(cx);
2718 }
2719 });
2720 }
2721
2722 fn paint_diff_hunks(
2723 gutter_bounds: Bounds<Pixels>,
2724 layout: &EditorLayout,
2725 cx: &mut WindowContext,
2726 ) {
2727 if layout.display_hunks.is_empty() {
2728 return;
2729 }
2730
2731 let line_height = layout.position_map.line_height;
2732 cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
2733 for (hunk, hitbox) in &layout.display_hunks {
2734 let hunk_to_paint = match hunk {
2735 DisplayDiffHunk::Folded { .. } => {
2736 let hunk_bounds = Self::diff_hunk_bounds(
2737 &layout.position_map.snapshot,
2738 line_height,
2739 gutter_bounds,
2740 &hunk,
2741 );
2742 Some((
2743 hunk_bounds,
2744 cx.theme().status().modified,
2745 Corners::all(1. * line_height),
2746 ))
2747 }
2748 DisplayDiffHunk::Unfolded { status, .. } => {
2749 hitbox.as_ref().map(|hunk_hitbox| match status {
2750 DiffHunkStatus::Added => (
2751 hunk_hitbox.bounds,
2752 cx.theme().status().created,
2753 Corners::all(0.05 * line_height),
2754 ),
2755 DiffHunkStatus::Modified => (
2756 hunk_hitbox.bounds,
2757 cx.theme().status().modified,
2758 Corners::all(0.05 * line_height),
2759 ),
2760 DiffHunkStatus::Removed => (
2761 hunk_hitbox.bounds,
2762 cx.theme().status().deleted,
2763 Corners::all(1. * line_height),
2764 ),
2765 })
2766 }
2767 };
2768
2769 if let Some((hunk_bounds, background_color, corner_radii)) = hunk_to_paint {
2770 cx.paint_quad(quad(
2771 hunk_bounds,
2772 corner_radii,
2773 background_color,
2774 Edges::default(),
2775 transparent_black(),
2776 ));
2777 }
2778 }
2779 });
2780 }
2781
2782 fn diff_hunk_bounds(
2783 snapshot: &EditorSnapshot,
2784 line_height: Pixels,
2785 bounds: Bounds<Pixels>,
2786 hunk: &DisplayDiffHunk,
2787 ) -> Bounds<Pixels> {
2788 let scroll_position = snapshot.scroll_position();
2789 let scroll_top = scroll_position.y * line_height;
2790
2791 match hunk {
2792 DisplayDiffHunk::Folded { display_row, .. } => {
2793 let start_y = display_row.as_f32() * line_height - scroll_top;
2794 let end_y = start_y + line_height;
2795
2796 let width = 0.275 * line_height;
2797 let highlight_origin = bounds.origin + point(-width, start_y);
2798 let highlight_size = size(width * 2., end_y - start_y);
2799 Bounds::new(highlight_origin, highlight_size)
2800 }
2801 DisplayDiffHunk::Unfolded {
2802 display_row_range,
2803 status,
2804 ..
2805 } => match status {
2806 DiffHunkStatus::Added | DiffHunkStatus::Modified => {
2807 let start_row = display_row_range.start;
2808 let end_row = display_row_range.end;
2809 // If we're in a multibuffer, row range span might include an
2810 // excerpt header, so if we were to draw the marker straight away,
2811 // the hunk might include the rows of that header.
2812 // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
2813 // Instead, we simply check whether the range we're dealing with includes
2814 // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header.
2815 let end_row_in_current_excerpt = snapshot
2816 .blocks_in_range(start_row..end_row)
2817 .find_map(|(start_row, block)| {
2818 if matches!(block, TransformBlock::ExcerptHeader { .. }) {
2819 Some(start_row)
2820 } else {
2821 None
2822 }
2823 })
2824 .unwrap_or(end_row);
2825
2826 let start_y = start_row.as_f32() * line_height - scroll_top;
2827 let end_y = end_row_in_current_excerpt.as_f32() * line_height - scroll_top;
2828
2829 let width = 0.275 * line_height;
2830 let highlight_origin = bounds.origin + point(-width, start_y);
2831 let highlight_size = size(width * 2., end_y - start_y);
2832 Bounds::new(highlight_origin, highlight_size)
2833 }
2834 DiffHunkStatus::Removed => {
2835 let row = display_row_range.start;
2836
2837 let offset = line_height / 2.;
2838 let start_y = row.as_f32() * line_height - offset - scroll_top;
2839 let end_y = start_y + line_height;
2840
2841 let width = 0.35 * line_height;
2842 let highlight_origin = bounds.origin + point(-width, start_y);
2843 let highlight_size = size(width * 2., end_y - start_y);
2844 Bounds::new(highlight_origin, highlight_size)
2845 }
2846 },
2847 }
2848 }
2849
2850 fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut WindowContext) {
2851 let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
2852 return;
2853 };
2854
2855 cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
2856 for mut blame_element in blamed_display_rows.into_iter() {
2857 blame_element.paint(cx);
2858 }
2859 })
2860 }
2861
2862 fn paint_text(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
2863 cx.with_content_mask(
2864 Some(ContentMask {
2865 bounds: layout.text_hitbox.bounds,
2866 }),
2867 |cx| {
2868 let cursor_style = if self
2869 .editor
2870 .read(cx)
2871 .hovered_link_state
2872 .as_ref()
2873 .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
2874 {
2875 CursorStyle::PointingHand
2876 } else {
2877 CursorStyle::IBeam
2878 };
2879 cx.set_cursor_style(cursor_style, &layout.text_hitbox);
2880
2881 let invisible_display_ranges = self.paint_highlights(layout, cx);
2882 self.paint_lines(&invisible_display_ranges, layout, cx);
2883 self.paint_redactions(layout, cx);
2884 self.paint_cursors(layout, cx);
2885 self.paint_inline_blame(layout, cx);
2886 cx.with_element_namespace("flap_trailers", |cx| {
2887 for trailer in layout.flap_trailers.iter_mut().flatten() {
2888 trailer.element.paint(cx);
2889 }
2890 });
2891 },
2892 )
2893 }
2894
2895 fn paint_highlights(
2896 &mut self,
2897 layout: &mut EditorLayout,
2898 cx: &mut WindowContext,
2899 ) -> SmallVec<[Range<DisplayPoint>; 32]> {
2900 cx.paint_layer(layout.text_hitbox.bounds, |cx| {
2901 let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
2902 let line_end_overshoot = 0.15 * layout.position_map.line_height;
2903 for (range, color) in &layout.highlighted_ranges {
2904 self.paint_highlighted_range(
2905 range.clone(),
2906 *color,
2907 Pixels::ZERO,
2908 line_end_overshoot,
2909 layout,
2910 cx,
2911 );
2912 }
2913
2914 let corner_radius = 0.15 * layout.position_map.line_height;
2915
2916 for (player_color, selections) in &layout.selections {
2917 for selection in selections.into_iter() {
2918 self.paint_highlighted_range(
2919 selection.range.clone(),
2920 player_color.selection,
2921 corner_radius,
2922 corner_radius * 2.,
2923 layout,
2924 cx,
2925 );
2926
2927 if selection.is_local && !selection.range.is_empty() {
2928 invisible_display_ranges.push(selection.range.clone());
2929 }
2930 }
2931 }
2932 invisible_display_ranges
2933 })
2934 }
2935
2936 fn paint_lines(
2937 &mut self,
2938 invisible_display_ranges: &[Range<DisplayPoint>],
2939 layout: &mut EditorLayout,
2940 cx: &mut WindowContext,
2941 ) {
2942 let whitespace_setting = self
2943 .editor
2944 .read(cx)
2945 .buffer
2946 .read(cx)
2947 .settings_at(0, cx)
2948 .show_whitespaces;
2949
2950 for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
2951 let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
2952 line_with_invisibles.draw(
2953 layout,
2954 row,
2955 layout.content_origin,
2956 whitespace_setting,
2957 invisible_display_ranges,
2958 cx,
2959 )
2960 }
2961
2962 for line_element in &mut layout.line_elements {
2963 line_element.paint(cx);
2964 }
2965 }
2966
2967 fn paint_redactions(&mut self, layout: &EditorLayout, cx: &mut WindowContext) {
2968 if layout.redacted_ranges.is_empty() {
2969 return;
2970 }
2971
2972 let line_end_overshoot = layout.line_end_overshoot();
2973
2974 // A softer than perfect black
2975 let redaction_color = gpui::rgb(0x0e1111);
2976
2977 cx.paint_layer(layout.text_hitbox.bounds, |cx| {
2978 for range in layout.redacted_ranges.iter() {
2979 self.paint_highlighted_range(
2980 range.clone(),
2981 redaction_color.into(),
2982 Pixels::ZERO,
2983 line_end_overshoot,
2984 layout,
2985 cx,
2986 );
2987 }
2988 });
2989 }
2990
2991 fn paint_cursors(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
2992 for cursor in &mut layout.visible_cursors {
2993 cursor.paint(layout.content_origin, cx);
2994 }
2995 }
2996
2997 fn paint_scrollbar(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
2998 let Some(scrollbar_layout) = layout.scrollbar_layout.as_ref() else {
2999 return;
3000 };
3001
3002 let thumb_bounds = scrollbar_layout.thumb_bounds();
3003 if scrollbar_layout.visible {
3004 cx.paint_layer(scrollbar_layout.hitbox.bounds, |cx| {
3005 cx.paint_quad(quad(
3006 scrollbar_layout.hitbox.bounds,
3007 Corners::default(),
3008 cx.theme().colors().scrollbar_track_background,
3009 Edges {
3010 top: Pixels::ZERO,
3011 right: Pixels::ZERO,
3012 bottom: Pixels::ZERO,
3013 left: ScrollbarLayout::BORDER_WIDTH,
3014 },
3015 cx.theme().colors().scrollbar_track_border,
3016 ));
3017
3018 let fast_markers =
3019 self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx);
3020 // Refresh slow scrollbar markers in the background. Below, we paint whatever markers have already been computed.
3021 self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, cx);
3022
3023 let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone();
3024 for marker in markers.iter().chain(&fast_markers) {
3025 let mut marker = marker.clone();
3026 marker.bounds.origin += scrollbar_layout.hitbox.origin;
3027 cx.paint_quad(marker);
3028 }
3029
3030 cx.paint_quad(quad(
3031 thumb_bounds,
3032 Corners::default(),
3033 cx.theme().colors().scrollbar_thumb_background,
3034 Edges {
3035 top: Pixels::ZERO,
3036 right: Pixels::ZERO,
3037 bottom: Pixels::ZERO,
3038 left: ScrollbarLayout::BORDER_WIDTH,
3039 },
3040 cx.theme().colors().scrollbar_thumb_border,
3041 ));
3042 });
3043 }
3044
3045 cx.set_cursor_style(CursorStyle::Arrow, &scrollbar_layout.hitbox);
3046
3047 let row_height = scrollbar_layout.row_height;
3048 let row_range = scrollbar_layout.visible_row_range.clone();
3049
3050 cx.on_mouse_event({
3051 let editor = self.editor.clone();
3052 let hitbox = scrollbar_layout.hitbox.clone();
3053 let mut mouse_position = cx.mouse_position();
3054 move |event: &MouseMoveEvent, phase, cx| {
3055 if phase == DispatchPhase::Capture {
3056 return;
3057 }
3058
3059 editor.update(cx, |editor, cx| {
3060 if event.pressed_button == Some(MouseButton::Left)
3061 && editor.scroll_manager.is_dragging_scrollbar()
3062 {
3063 let y = mouse_position.y;
3064 let new_y = event.position.y;
3065 if (hitbox.top()..hitbox.bottom()).contains(&y) {
3066 let mut position = editor.scroll_position(cx);
3067 position.y += (new_y - y) / row_height;
3068 if position.y < 0.0 {
3069 position.y = 0.0;
3070 }
3071 editor.set_scroll_position(position, cx);
3072 }
3073
3074 cx.stop_propagation();
3075 } else {
3076 editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
3077 if hitbox.is_hovered(cx) {
3078 editor.scroll_manager.show_scrollbar(cx);
3079 }
3080 }
3081 mouse_position = event.position;
3082 })
3083 }
3084 });
3085
3086 if self.editor.read(cx).scroll_manager.is_dragging_scrollbar() {
3087 cx.on_mouse_event({
3088 let editor = self.editor.clone();
3089 move |_: &MouseUpEvent, phase, cx| {
3090 if phase == DispatchPhase::Capture {
3091 return;
3092 }
3093
3094 editor.update(cx, |editor, cx| {
3095 editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
3096 cx.stop_propagation();
3097 });
3098 }
3099 });
3100 } else {
3101 cx.on_mouse_event({
3102 let editor = self.editor.clone();
3103 let hitbox = scrollbar_layout.hitbox.clone();
3104 move |event: &MouseDownEvent, phase, cx| {
3105 if phase == DispatchPhase::Capture || !hitbox.is_hovered(cx) {
3106 return;
3107 }
3108
3109 editor.update(cx, |editor, cx| {
3110 editor.scroll_manager.set_is_dragging_scrollbar(true, cx);
3111
3112 let y = event.position.y;
3113 if y < thumb_bounds.top() || thumb_bounds.bottom() < y {
3114 let center_row = ((y - hitbox.top()) / row_height).round() as u32;
3115 let top_row = center_row
3116 .saturating_sub((row_range.end - row_range.start) as u32 / 2);
3117 let mut position = editor.scroll_position(cx);
3118 position.y = top_row as f32;
3119 editor.set_scroll_position(position, cx);
3120 } else {
3121 editor.scroll_manager.show_scrollbar(cx);
3122 }
3123
3124 cx.stop_propagation();
3125 });
3126 }
3127 });
3128 }
3129 }
3130
3131 fn collect_fast_scrollbar_markers(
3132 &self,
3133 layout: &EditorLayout,
3134 scrollbar_layout: &ScrollbarLayout,
3135 cx: &mut WindowContext,
3136 ) -> Vec<PaintQuad> {
3137 const LIMIT: usize = 100;
3138 if !EditorSettings::get_global(cx).scrollbar.cursors || layout.cursors.len() > LIMIT {
3139 return vec![];
3140 }
3141 let cursor_ranges = layout
3142 .cursors
3143 .iter()
3144 .map(|(point, color)| ColoredRange {
3145 start: point.row(),
3146 end: point.row(),
3147 color: *color,
3148 })
3149 .collect_vec();
3150 scrollbar_layout.marker_quads_for_ranges(cursor_ranges, None)
3151 }
3152
3153 fn refresh_slow_scrollbar_markers(
3154 &self,
3155 layout: &EditorLayout,
3156 scrollbar_layout: &ScrollbarLayout,
3157 cx: &mut WindowContext,
3158 ) {
3159 self.editor.update(cx, |editor, cx| {
3160 if !editor.is_singleton(cx)
3161 || !editor
3162 .scrollbar_marker_state
3163 .should_refresh(scrollbar_layout.hitbox.size)
3164 {
3165 return;
3166 }
3167
3168 let scrollbar_layout = scrollbar_layout.clone();
3169 let background_highlights = editor.background_highlights.clone();
3170 let snapshot = layout.position_map.snapshot.clone();
3171 let theme = cx.theme().clone();
3172 let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
3173
3174 editor.scrollbar_marker_state.dirty = false;
3175 editor.scrollbar_marker_state.pending_refresh =
3176 Some(cx.spawn(|editor, mut cx| async move {
3177 let scrollbar_size = scrollbar_layout.hitbox.size;
3178 let scrollbar_markers = cx
3179 .background_executor()
3180 .spawn(async move {
3181 let max_point = snapshot.display_snapshot.buffer_snapshot.max_point();
3182 let mut marker_quads = Vec::new();
3183 if scrollbar_settings.git_diff {
3184 let marker_row_ranges = snapshot
3185 .buffer_snapshot
3186 .git_diff_hunks_in_range(
3187 MultiBufferRow::MIN..MultiBufferRow::MAX,
3188 )
3189 .map(|hunk| {
3190 let start_display_row =
3191 MultiBufferPoint::new(hunk.associated_range.start.0, 0)
3192 .to_display_point(&snapshot.display_snapshot)
3193 .row();
3194 let mut end_display_row =
3195 MultiBufferPoint::new(hunk.associated_range.end.0, 0)
3196 .to_display_point(&snapshot.display_snapshot)
3197 .row();
3198 if end_display_row != start_display_row {
3199 end_display_row.0 -= 1;
3200 }
3201 let color = match hunk_status(&hunk) {
3202 DiffHunkStatus::Added => theme.status().created,
3203 DiffHunkStatus::Modified => theme.status().modified,
3204 DiffHunkStatus::Removed => theme.status().deleted,
3205 };
3206 ColoredRange {
3207 start: start_display_row,
3208 end: end_display_row,
3209 color,
3210 }
3211 });
3212
3213 marker_quads.extend(
3214 scrollbar_layout
3215 .marker_quads_for_ranges(marker_row_ranges, Some(0)),
3216 );
3217 }
3218
3219 for (background_highlight_id, (_, background_ranges)) in
3220 background_highlights.iter()
3221 {
3222 let is_search_highlights = *background_highlight_id
3223 == TypeId::of::<BufferSearchHighlights>();
3224 let is_symbol_occurrences = *background_highlight_id
3225 == TypeId::of::<DocumentHighlightRead>()
3226 || *background_highlight_id
3227 == TypeId::of::<DocumentHighlightWrite>();
3228 if (is_search_highlights && scrollbar_settings.search_results)
3229 || (is_symbol_occurrences && scrollbar_settings.selected_symbol)
3230 {
3231 let mut color = theme.status().info;
3232 if is_symbol_occurrences {
3233 color.fade_out(0.5);
3234 }
3235 let marker_row_ranges =
3236 background_ranges.into_iter().map(|range| {
3237 let display_start = range
3238 .start
3239 .to_display_point(&snapshot.display_snapshot);
3240 let display_end = range
3241 .end
3242 .to_display_point(&snapshot.display_snapshot);
3243 ColoredRange {
3244 start: display_start.row(),
3245 end: display_end.row(),
3246 color,
3247 }
3248 });
3249 marker_quads.extend(
3250 scrollbar_layout
3251 .marker_quads_for_ranges(marker_row_ranges, Some(1)),
3252 );
3253 }
3254 }
3255
3256 if scrollbar_settings.diagnostics {
3257 let diagnostics = snapshot
3258 .buffer_snapshot
3259 .diagnostics_in_range::<_, Point>(
3260 Point::zero()..max_point,
3261 false,
3262 )
3263 // We want to sort by severity, in order to paint the most severe diagnostics last.
3264 .sorted_by_key(|diagnostic| {
3265 std::cmp::Reverse(diagnostic.diagnostic.severity)
3266 });
3267
3268 let marker_row_ranges = diagnostics.into_iter().map(|diagnostic| {
3269 let start_display = diagnostic
3270 .range
3271 .start
3272 .to_display_point(&snapshot.display_snapshot);
3273 let end_display = diagnostic
3274 .range
3275 .end
3276 .to_display_point(&snapshot.display_snapshot);
3277 let color = match diagnostic.diagnostic.severity {
3278 DiagnosticSeverity::ERROR => theme.status().error,
3279 DiagnosticSeverity::WARNING => theme.status().warning,
3280 DiagnosticSeverity::INFORMATION => theme.status().info,
3281 _ => theme.status().hint,
3282 };
3283 ColoredRange {
3284 start: start_display.row(),
3285 end: end_display.row(),
3286 color,
3287 }
3288 });
3289 marker_quads.extend(
3290 scrollbar_layout
3291 .marker_quads_for_ranges(marker_row_ranges, Some(2)),
3292 );
3293 }
3294
3295 Arc::from(marker_quads)
3296 })
3297 .await;
3298
3299 editor.update(&mut cx, |editor, cx| {
3300 editor.scrollbar_marker_state.markers = scrollbar_markers;
3301 editor.scrollbar_marker_state.scrollbar_size = scrollbar_size;
3302 editor.scrollbar_marker_state.pending_refresh = None;
3303 cx.notify();
3304 })?;
3305
3306 Ok(())
3307 }));
3308 });
3309 }
3310
3311 #[allow(clippy::too_many_arguments)]
3312 fn paint_highlighted_range(
3313 &self,
3314 range: Range<DisplayPoint>,
3315 color: Hsla,
3316 corner_radius: Pixels,
3317 line_end_overshoot: Pixels,
3318 layout: &EditorLayout,
3319 cx: &mut WindowContext,
3320 ) {
3321 let start_row = layout.visible_display_row_range.start;
3322 let end_row = layout.visible_display_row_range.end;
3323 if range.start != range.end {
3324 let row_range = if range.end.column() == 0 {
3325 cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
3326 } else {
3327 cmp::max(range.start.row(), start_row)
3328 ..cmp::min(range.end.row().next_row(), end_row)
3329 };
3330
3331 let highlighted_range = HighlightedRange {
3332 color,
3333 line_height: layout.position_map.line_height,
3334 corner_radius,
3335 start_y: layout.content_origin.y
3336 + row_range.start.as_f32() * layout.position_map.line_height
3337 - layout.position_map.scroll_pixel_position.y,
3338 lines: row_range
3339 .iter_rows()
3340 .map(|row| {
3341 let line_layout =
3342 &layout.position_map.line_layouts[row.minus(start_row) as usize];
3343 HighlightedRangeLine {
3344 start_x: if row == range.start.row() {
3345 layout.content_origin.x
3346 + line_layout.x_for_index(range.start.column() as usize)
3347 - layout.position_map.scroll_pixel_position.x
3348 } else {
3349 layout.content_origin.x
3350 - layout.position_map.scroll_pixel_position.x
3351 },
3352 end_x: if row == range.end.row() {
3353 layout.content_origin.x
3354 + line_layout.x_for_index(range.end.column() as usize)
3355 - layout.position_map.scroll_pixel_position.x
3356 } else {
3357 layout.content_origin.x + line_layout.width + line_end_overshoot
3358 - layout.position_map.scroll_pixel_position.x
3359 },
3360 }
3361 })
3362 .collect(),
3363 };
3364
3365 highlighted_range.paint(layout.text_hitbox.bounds, cx);
3366 }
3367 }
3368
3369 fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
3370 if let Some(mut inline_blame) = layout.inline_blame.take() {
3371 cx.paint_layer(layout.text_hitbox.bounds, |cx| {
3372 inline_blame.paint(cx);
3373 })
3374 }
3375 }
3376
3377 fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
3378 for mut block in layout.blocks.drain(..) {
3379 block.element.paint(cx);
3380 }
3381 }
3382
3383 fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
3384 if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
3385 mouse_context_menu.paint(cx);
3386 }
3387 }
3388
3389 fn paint_scroll_wheel_listener(&mut self, layout: &EditorLayout, cx: &mut WindowContext) {
3390 cx.on_mouse_event({
3391 let position_map = layout.position_map.clone();
3392 let editor = self.editor.clone();
3393 let hitbox = layout.hitbox.clone();
3394 let mut delta = ScrollDelta::default();
3395
3396 // Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't
3397 // accidentally turn off their scrolling.
3398 let scroll_sensitivity = EditorSettings::get_global(cx).scroll_sensitivity.max(0.01);
3399
3400 move |event: &ScrollWheelEvent, phase, cx| {
3401 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
3402 delta = delta.coalesce(event.delta);
3403 editor.update(cx, |editor, cx| {
3404 let position_map: &PositionMap = &position_map;
3405
3406 let line_height = position_map.line_height;
3407 let max_glyph_width = position_map.em_width;
3408 let (delta, axis) = match delta {
3409 gpui::ScrollDelta::Pixels(mut pixels) => {
3410 //Trackpad
3411 let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
3412 (pixels, axis)
3413 }
3414
3415 gpui::ScrollDelta::Lines(lines) => {
3416 //Not trackpad
3417 let pixels =
3418 point(lines.x * max_glyph_width, lines.y * line_height);
3419 (pixels, None)
3420 }
3421 };
3422
3423 let current_scroll_position = position_map.snapshot.scroll_position();
3424 let x = (current_scroll_position.x * max_glyph_width
3425 - (delta.x * scroll_sensitivity))
3426 / max_glyph_width;
3427 let y = (current_scroll_position.y * line_height
3428 - (delta.y * scroll_sensitivity))
3429 / line_height;
3430 let mut scroll_position =
3431 point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
3432 let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll();
3433 if forbid_vertical_scroll {
3434 scroll_position.y = current_scroll_position.y;
3435 if scroll_position == current_scroll_position {
3436 return;
3437 }
3438 }
3439 editor.scroll(scroll_position, axis, cx);
3440 cx.stop_propagation();
3441 });
3442 }
3443 }
3444 });
3445 }
3446
3447 fn paint_mouse_listeners(
3448 &mut self,
3449 layout: &EditorLayout,
3450 hovered_hunk: Option<HunkToExpand>,
3451 cx: &mut WindowContext,
3452 ) {
3453 self.paint_scroll_wheel_listener(layout, cx);
3454
3455 cx.on_mouse_event({
3456 let position_map = layout.position_map.clone();
3457 let editor = self.editor.clone();
3458 let text_hitbox = layout.text_hitbox.clone();
3459 let gutter_hitbox = layout.gutter_hitbox.clone();
3460
3461 move |event: &MouseDownEvent, phase, cx| {
3462 if phase == DispatchPhase::Bubble {
3463 match event.button {
3464 MouseButton::Left => editor.update(cx, |editor, cx| {
3465 Self::mouse_left_down(
3466 editor,
3467 event,
3468 hovered_hunk.as_ref(),
3469 &position_map,
3470 &text_hitbox,
3471 &gutter_hitbox,
3472 cx,
3473 );
3474 }),
3475 MouseButton::Right => editor.update(cx, |editor, cx| {
3476 Self::mouse_right_down(editor, event, &position_map, &text_hitbox, cx);
3477 }),
3478 MouseButton::Middle => editor.update(cx, |editor, cx| {
3479 Self::mouse_middle_down(editor, event, &position_map, &text_hitbox, cx);
3480 }),
3481 _ => {}
3482 };
3483 }
3484 }
3485 });
3486
3487 cx.on_mouse_event({
3488 let editor = self.editor.clone();
3489 let position_map = layout.position_map.clone();
3490 let text_hitbox = layout.text_hitbox.clone();
3491
3492 move |event: &MouseUpEvent, phase, cx| {
3493 if phase == DispatchPhase::Bubble {
3494 editor.update(cx, |editor, cx| {
3495 Self::mouse_up(editor, event, &position_map, &text_hitbox, cx)
3496 });
3497 }
3498 }
3499 });
3500 cx.on_mouse_event({
3501 let position_map = layout.position_map.clone();
3502 let editor = self.editor.clone();
3503 let text_hitbox = layout.text_hitbox.clone();
3504 let gutter_hitbox = layout.gutter_hitbox.clone();
3505
3506 move |event: &MouseMoveEvent, phase, cx| {
3507 if phase == DispatchPhase::Bubble {
3508 editor.update(cx, |editor, cx| {
3509 if event.pressed_button == Some(MouseButton::Left)
3510 || event.pressed_button == Some(MouseButton::Middle)
3511 {
3512 Self::mouse_dragged(
3513 editor,
3514 event,
3515 &position_map,
3516 text_hitbox.bounds,
3517 cx,
3518 )
3519 }
3520
3521 Self::mouse_moved(
3522 editor,
3523 event,
3524 &position_map,
3525 &text_hitbox,
3526 &gutter_hitbox,
3527 cx,
3528 )
3529 });
3530 }
3531 }
3532 });
3533 }
3534
3535 fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels {
3536 bounds.upper_right().x - self.style.scrollbar_width
3537 }
3538
3539 fn column_pixels(&self, column: usize, cx: &WindowContext) -> Pixels {
3540 let style = &self.style;
3541 let font_size = style.text.font_size.to_pixels(cx.rem_size());
3542 let layout = cx
3543 .text_system()
3544 .shape_line(
3545 SharedString::from(" ".repeat(column)),
3546 font_size,
3547 &[TextRun {
3548 len: column,
3549 font: style.text.font(),
3550 color: Hsla::default(),
3551 background_color: None,
3552 underline: None,
3553 strikethrough: None,
3554 }],
3555 )
3556 .unwrap();
3557
3558 layout.width
3559 }
3560
3561 fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels {
3562 let digit_count = snapshot
3563 .max_buffer_row()
3564 .next_row()
3565 .as_f32()
3566 .log10()
3567 .floor() as usize
3568 + 1;
3569 self.column_pixels(digit_count, cx)
3570 }
3571}
3572
3573fn prepaint_gutter_button(
3574 button: IconButton,
3575 row: DisplayRow,
3576 line_height: Pixels,
3577 gutter_dimensions: &GutterDimensions,
3578 scroll_pixel_position: gpui::Point<Pixels>,
3579 gutter_hitbox: &Hitbox,
3580 cx: &mut WindowContext<'_>,
3581) -> AnyElement {
3582 let mut button = button.into_any_element();
3583 let available_space = size(
3584 AvailableSpace::MinContent,
3585 AvailableSpace::Definite(line_height),
3586 );
3587 let indicator_size = button.layout_as_root(available_space, cx);
3588
3589 let blame_width = gutter_dimensions
3590 .git_blame_entries_width
3591 .unwrap_or(Pixels::ZERO);
3592
3593 let mut x = blame_width;
3594 let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
3595 - indicator_size.width
3596 - blame_width;
3597 x += available_width / 2.;
3598
3599 let mut y = row.as_f32() * line_height - scroll_pixel_position.y;
3600 y += (line_height - indicator_size.height) / 2.;
3601
3602 button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
3603 button
3604}
3605
3606fn render_inline_blame_entry(
3607 blame: &gpui::Model<GitBlame>,
3608 blame_entry: BlameEntry,
3609 style: &EditorStyle,
3610 workspace: Option<WeakView<Workspace>>,
3611 cx: &mut WindowContext<'_>,
3612) -> AnyElement {
3613 let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
3614
3615 let author = blame_entry.author.as_deref().unwrap_or_default();
3616 let text = format!("{}, {}", author, relative_timestamp);
3617
3618 let details = blame.read(cx).details_for_entry(&blame_entry);
3619
3620 let tooltip = cx.new_view(|_| BlameEntryTooltip::new(blame_entry, details, style, workspace));
3621
3622 h_flex()
3623 .id("inline-blame")
3624 .w_full()
3625 .font_family(style.text.font().family)
3626 .text_color(cx.theme().status().hint)
3627 .line_height(style.text.line_height)
3628 .child(Icon::new(IconName::FileGit).color(Color::Hint))
3629 .child(text)
3630 .gap_2()
3631 .hoverable_tooltip(move |_| tooltip.clone().into())
3632 .into_any()
3633}
3634
3635fn render_blame_entry(
3636 ix: usize,
3637 blame: &gpui::Model<GitBlame>,
3638 blame_entry: BlameEntry,
3639 style: &EditorStyle,
3640 last_used_color: &mut Option<(PlayerColor, Oid)>,
3641 editor: View<Editor>,
3642 cx: &mut WindowContext<'_>,
3643) -> AnyElement {
3644 let mut sha_color = cx
3645 .theme()
3646 .players()
3647 .color_for_participant(blame_entry.sha.into());
3648 // If the last color we used is the same as the one we get for this line, but
3649 // the commit SHAs are different, then we try again to get a different color.
3650 match *last_used_color {
3651 Some((color, sha)) if sha != blame_entry.sha && color.cursor == sha_color.cursor => {
3652 let index: u32 = blame_entry.sha.into();
3653 sha_color = cx.theme().players().color_for_participant(index + 1);
3654 }
3655 _ => {}
3656 };
3657 last_used_color.replace((sha_color, blame_entry.sha));
3658
3659 let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
3660
3661 let short_commit_id = blame_entry.sha.display_short();
3662
3663 let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
3664 let name = util::truncate_and_trailoff(author_name, 20);
3665
3666 let details = blame.read(cx).details_for_entry(&blame_entry);
3667
3668 let workspace = editor.read(cx).workspace.as_ref().map(|(w, _)| w.clone());
3669
3670 let tooltip = cx.new_view(|_| {
3671 BlameEntryTooltip::new(blame_entry.clone(), details.clone(), style, workspace)
3672 });
3673
3674 h_flex()
3675 .w_full()
3676 .font_family(style.text.font().family)
3677 .line_height(style.text.line_height)
3678 .id(("blame", ix))
3679 .children([
3680 div()
3681 .text_color(sha_color.cursor)
3682 .child(short_commit_id)
3683 .mr_2(),
3684 div()
3685 .w_full()
3686 .h_flex()
3687 .justify_between()
3688 .text_color(cx.theme().status().hint)
3689 .child(name)
3690 .child(relative_timestamp),
3691 ])
3692 .on_mouse_down(MouseButton::Right, {
3693 let blame_entry = blame_entry.clone();
3694 let details = details.clone();
3695 move |event, cx| {
3696 deploy_blame_entry_context_menu(
3697 &blame_entry,
3698 details.as_ref(),
3699 editor.clone(),
3700 event.position,
3701 cx,
3702 );
3703 }
3704 })
3705 .hover(|style| style.bg(cx.theme().colors().element_hover))
3706 .when_some(
3707 details.and_then(|details| details.permalink),
3708 |this, url| {
3709 let url = url.clone();
3710 this.cursor_pointer().on_click(move |_, cx| {
3711 cx.stop_propagation();
3712 cx.open_url(url.as_str())
3713 })
3714 },
3715 )
3716 .hoverable_tooltip(move |_| tooltip.clone().into())
3717 .into_any()
3718}
3719
3720fn deploy_blame_entry_context_menu(
3721 blame_entry: &BlameEntry,
3722 details: Option<&CommitDetails>,
3723 editor: View<Editor>,
3724 position: gpui::Point<Pixels>,
3725 cx: &mut WindowContext<'_>,
3726) {
3727 let context_menu = ContextMenu::build(cx, move |this, _| {
3728 let sha = format!("{}", blame_entry.sha);
3729 this.entry("Copy commit SHA", None, move |cx| {
3730 cx.write_to_clipboard(ClipboardItem::new(sha.clone()));
3731 })
3732 .when_some(
3733 details.and_then(|details| details.permalink.clone()),
3734 |this, url| this.entry("Open permalink", None, move |cx| cx.open_url(url.as_str())),
3735 )
3736 });
3737
3738 editor.update(cx, move |editor, cx| {
3739 editor.mouse_context_menu = Some(MouseContextMenu::new(position, context_menu, cx));
3740 cx.notify();
3741 });
3742}
3743
3744#[derive(Debug)]
3745pub(crate) struct LineWithInvisibles {
3746 fragments: SmallVec<[LineFragment; 1]>,
3747 invisibles: Vec<Invisible>,
3748 len: usize,
3749 width: Pixels,
3750 font_size: Pixels,
3751}
3752
3753#[allow(clippy::large_enum_variant)]
3754enum LineFragment {
3755 Text(ShapedLine),
3756 Element {
3757 element: Option<AnyElement>,
3758 width: Pixels,
3759 len: usize,
3760 },
3761}
3762
3763impl fmt::Debug for LineFragment {
3764 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3765 match self {
3766 LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
3767 LineFragment::Element { width, len, .. } => f
3768 .debug_struct("Element")
3769 .field("width", width)
3770 .field("len", len)
3771 .finish(),
3772 }
3773 }
3774}
3775
3776impl LineWithInvisibles {
3777 fn from_chunks<'a>(
3778 chunks: impl Iterator<Item = HighlightedChunk<'a>>,
3779 text_style: &TextStyle,
3780 max_line_len: usize,
3781 max_line_count: usize,
3782 line_number_layouts: &[Option<ShapedLine>],
3783 editor_mode: EditorMode,
3784 cx: &mut WindowContext,
3785 ) -> Vec<Self> {
3786 let mut layouts = Vec::with_capacity(max_line_count);
3787 let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
3788 let mut line = String::new();
3789 let mut invisibles = Vec::new();
3790 let mut width = Pixels::ZERO;
3791 let mut len = 0;
3792 let mut styles = Vec::new();
3793 let mut non_whitespace_added = false;
3794 let mut row = 0;
3795 let mut line_exceeded_max_len = false;
3796 let font_size = text_style.font_size.to_pixels(cx.rem_size());
3797
3798 let ellipsis = SharedString::from("⋯");
3799
3800 for highlighted_chunk in chunks.chain([HighlightedChunk {
3801 text: "\n",
3802 style: None,
3803 is_tab: false,
3804 renderer: None,
3805 }]) {
3806 if let Some(renderer) = highlighted_chunk.renderer {
3807 if !line.is_empty() {
3808 let shaped_line = cx
3809 .text_system()
3810 .shape_line(line.clone().into(), font_size, &styles)
3811 .unwrap();
3812 width += shaped_line.width;
3813 len += shaped_line.len;
3814 fragments.push(LineFragment::Text(shaped_line));
3815 line.clear();
3816 styles.clear();
3817 }
3818
3819 let available_width = if renderer.constrain_width {
3820 let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
3821 ellipsis.clone()
3822 } else {
3823 SharedString::from(Arc::from(highlighted_chunk.text))
3824 };
3825 let shaped_line = cx
3826 .text_system()
3827 .shape_line(
3828 chunk,
3829 font_size,
3830 &[text_style.to_run(highlighted_chunk.text.len())],
3831 )
3832 .unwrap();
3833 AvailableSpace::Definite(shaped_line.width)
3834 } else {
3835 AvailableSpace::MinContent
3836 };
3837
3838 let mut element = (renderer.render)(cx);
3839 let line_height = text_style.line_height_in_pixels(cx.rem_size());
3840 let size = element.layout_as_root(
3841 size(available_width, AvailableSpace::Definite(line_height)),
3842 cx,
3843 );
3844
3845 width += size.width;
3846 len += highlighted_chunk.text.len();
3847 fragments.push(LineFragment::Element {
3848 element: Some(element),
3849 width: size.width,
3850 len: highlighted_chunk.text.len(),
3851 });
3852 } else {
3853 for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
3854 if ix > 0 {
3855 let shaped_line = cx
3856 .text_system()
3857 .shape_line(line.clone().into(), font_size, &styles)
3858 .unwrap();
3859 width += shaped_line.width;
3860 len += shaped_line.len;
3861 fragments.push(LineFragment::Text(shaped_line));
3862 layouts.push(Self {
3863 width: mem::take(&mut width),
3864 len: mem::take(&mut len),
3865 fragments: mem::take(&mut fragments),
3866 invisibles: std::mem::take(&mut invisibles),
3867 font_size,
3868 });
3869
3870 line.clear();
3871 styles.clear();
3872 row += 1;
3873 line_exceeded_max_len = false;
3874 non_whitespace_added = false;
3875 if row == max_line_count {
3876 return layouts;
3877 }
3878 }
3879
3880 if !line_chunk.is_empty() && !line_exceeded_max_len {
3881 let text_style = if let Some(style) = highlighted_chunk.style {
3882 Cow::Owned(text_style.clone().highlight(style))
3883 } else {
3884 Cow::Borrowed(text_style)
3885 };
3886
3887 if line.len() + line_chunk.len() > max_line_len {
3888 let mut chunk_len = max_line_len - line.len();
3889 while !line_chunk.is_char_boundary(chunk_len) {
3890 chunk_len -= 1;
3891 }
3892 line_chunk = &line_chunk[..chunk_len];
3893 line_exceeded_max_len = true;
3894 }
3895
3896 styles.push(TextRun {
3897 len: line_chunk.len(),
3898 font: text_style.font(),
3899 color: text_style.color,
3900 background_color: text_style.background_color,
3901 underline: text_style.underline,
3902 strikethrough: text_style.strikethrough,
3903 });
3904
3905 if editor_mode == EditorMode::Full {
3906 // Line wrap pads its contents with fake whitespaces,
3907 // avoid printing them
3908 let inside_wrapped_string = line_number_layouts
3909 .get(row)
3910 .and_then(|layout| layout.as_ref())
3911 .is_none();
3912 if highlighted_chunk.is_tab {
3913 if non_whitespace_added || !inside_wrapped_string {
3914 invisibles.push(Invisible::Tab {
3915 line_start_offset: line.len(),
3916 });
3917 }
3918 } else {
3919 invisibles.extend(
3920 line_chunk
3921 .bytes()
3922 .enumerate()
3923 .filter(|(_, line_byte)| {
3924 let is_whitespace =
3925 (*line_byte as char).is_whitespace();
3926 non_whitespace_added |= !is_whitespace;
3927 is_whitespace
3928 && (non_whitespace_added || !inside_wrapped_string)
3929 })
3930 .map(|(whitespace_index, _)| Invisible::Whitespace {
3931 line_offset: line.len() + whitespace_index,
3932 }),
3933 )
3934 }
3935 }
3936
3937 line.push_str(line_chunk);
3938 }
3939 }
3940 }
3941 }
3942
3943 layouts
3944 }
3945
3946 fn prepaint(
3947 &mut self,
3948 line_height: Pixels,
3949 scroll_pixel_position: gpui::Point<Pixels>,
3950 row: DisplayRow,
3951 content_origin: gpui::Point<Pixels>,
3952 line_elements: &mut SmallVec<[AnyElement; 1]>,
3953 cx: &mut WindowContext,
3954 ) {
3955 let line_y = line_height * (row.as_f32() - scroll_pixel_position.y / line_height);
3956 let mut fragment_origin = content_origin + gpui::point(-scroll_pixel_position.x, line_y);
3957 for fragment in &mut self.fragments {
3958 match fragment {
3959 LineFragment::Text(line) => {
3960 fragment_origin.x += line.width;
3961 }
3962 LineFragment::Element { element, width, .. } => {
3963 let mut element = element
3964 .take()
3965 .expect("you can't prepaint LineWithInvisibles twice");
3966 element.prepaint_at(fragment_origin, cx);
3967 line_elements.push(element);
3968 fragment_origin.x += *width;
3969 }
3970 }
3971 }
3972 }
3973
3974 fn draw(
3975 &self,
3976 layout: &EditorLayout,
3977 row: DisplayRow,
3978 content_origin: gpui::Point<Pixels>,
3979 whitespace_setting: ShowWhitespaceSetting,
3980 selection_ranges: &[Range<DisplayPoint>],
3981 cx: &mut WindowContext,
3982 ) {
3983 let line_height = layout.position_map.line_height;
3984 let line_y = line_height
3985 * (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
3986
3987 let mut fragment_origin =
3988 content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
3989
3990 for fragment in &self.fragments {
3991 match fragment {
3992 LineFragment::Text(line) => {
3993 line.paint(fragment_origin, line_height, cx).log_err();
3994 fragment_origin.x += line.width;
3995 }
3996 LineFragment::Element { width, .. } => {
3997 fragment_origin.x += *width;
3998 }
3999 }
4000 }
4001
4002 self.draw_invisibles(
4003 &selection_ranges,
4004 layout,
4005 content_origin,
4006 line_y,
4007 row,
4008 line_height,
4009 whitespace_setting,
4010 cx,
4011 );
4012 }
4013
4014 #[allow(clippy::too_many_arguments)]
4015 fn draw_invisibles(
4016 &self,
4017 selection_ranges: &[Range<DisplayPoint>],
4018 layout: &EditorLayout,
4019 content_origin: gpui::Point<Pixels>,
4020 line_y: Pixels,
4021 row: DisplayRow,
4022 line_height: Pixels,
4023 whitespace_setting: ShowWhitespaceSetting,
4024 cx: &mut WindowContext,
4025 ) {
4026 let allowed_invisibles_regions = match whitespace_setting {
4027 ShowWhitespaceSetting::None => return,
4028 ShowWhitespaceSetting::Selection => Some(selection_ranges),
4029 ShowWhitespaceSetting::All => None,
4030 };
4031
4032 for invisible in &self.invisibles {
4033 let (&token_offset, invisible_symbol) = match invisible {
4034 Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
4035 Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
4036 };
4037
4038 let x_offset = self.x_for_index(token_offset);
4039 let invisible_offset =
4040 (layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0;
4041 let origin = content_origin
4042 + gpui::point(
4043 x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x,
4044 line_y,
4045 );
4046
4047 if let Some(allowed_regions) = allowed_invisibles_regions {
4048 let invisible_point = DisplayPoint::new(row, token_offset as u32);
4049 if !allowed_regions
4050 .iter()
4051 .any(|region| region.start <= invisible_point && invisible_point < region.end)
4052 {
4053 continue;
4054 }
4055 }
4056 invisible_symbol.paint(origin, line_height, cx).log_err();
4057 }
4058 }
4059
4060 pub fn x_for_index(&self, index: usize) -> Pixels {
4061 let mut fragment_start_x = Pixels::ZERO;
4062 let mut fragment_start_index = 0;
4063
4064 for fragment in &self.fragments {
4065 match fragment {
4066 LineFragment::Text(shaped_line) => {
4067 let fragment_end_index = fragment_start_index + shaped_line.len;
4068 if index < fragment_end_index {
4069 return fragment_start_x
4070 + shaped_line.x_for_index(index - fragment_start_index);
4071 }
4072 fragment_start_x += shaped_line.width;
4073 fragment_start_index = fragment_end_index;
4074 }
4075 LineFragment::Element { len, width, .. } => {
4076 let fragment_end_index = fragment_start_index + len;
4077 if index < fragment_end_index {
4078 return fragment_start_x;
4079 }
4080 fragment_start_x += *width;
4081 fragment_start_index = fragment_end_index;
4082 }
4083 }
4084 }
4085
4086 fragment_start_x
4087 }
4088
4089 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
4090 let mut fragment_start_x = Pixels::ZERO;
4091 let mut fragment_start_index = 0;
4092
4093 for fragment in &self.fragments {
4094 match fragment {
4095 LineFragment::Text(shaped_line) => {
4096 let fragment_end_x = fragment_start_x + shaped_line.width;
4097 if x < fragment_end_x {
4098 return Some(
4099 fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
4100 );
4101 }
4102 fragment_start_x = fragment_end_x;
4103 fragment_start_index += shaped_line.len;
4104 }
4105 LineFragment::Element { len, width, .. } => {
4106 let fragment_end_x = fragment_start_x + *width;
4107 if x < fragment_end_x {
4108 return Some(fragment_start_index);
4109 }
4110 fragment_start_index += len;
4111 fragment_start_x = fragment_end_x;
4112 }
4113 }
4114 }
4115
4116 None
4117 }
4118
4119 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
4120 let mut fragment_start_index = 0;
4121
4122 for fragment in &self.fragments {
4123 match fragment {
4124 LineFragment::Text(shaped_line) => {
4125 let fragment_end_index = fragment_start_index + shaped_line.len;
4126 if index < fragment_end_index {
4127 return shaped_line.font_id_for_index(index - fragment_start_index);
4128 }
4129 fragment_start_index = fragment_end_index;
4130 }
4131 LineFragment::Element { len, .. } => {
4132 let fragment_end_index = fragment_start_index + len;
4133 if index < fragment_end_index {
4134 return None;
4135 }
4136 fragment_start_index = fragment_end_index;
4137 }
4138 }
4139 }
4140
4141 None
4142 }
4143}
4144
4145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4146enum Invisible {
4147 Tab { line_start_offset: usize },
4148 Whitespace { line_offset: usize },
4149}
4150
4151impl EditorElement {
4152 /// Returns the rem size to use when rendering the [`EditorElement`].
4153 ///
4154 /// This allows UI elements to scale based on the `buffer_font_size`.
4155 fn rem_size(&self, cx: &WindowContext) -> Option<Pixels> {
4156 match self.editor.read(cx).mode {
4157 EditorMode::Full => {
4158 let buffer_font_size = self.style.text.font_size;
4159 match buffer_font_size {
4160 AbsoluteLength::Pixels(pixels) => {
4161 let rem_size_scale = {
4162 // Our default UI font size is 14px on a 16px base scale.
4163 // This means the default UI font size is 0.875rems.
4164 let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
4165
4166 // We then determine the delta between a single rem and the default font
4167 // size scale.
4168 let default_font_size_delta = 1. - default_font_size_scale;
4169
4170 // Finally, we add this delta to 1rem to get the scale factor that
4171 // should be used to scale up the UI.
4172 1. + default_font_size_delta
4173 };
4174
4175 Some(pixels * rem_size_scale)
4176 }
4177 AbsoluteLength::Rems(rems) => {
4178 Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into()))
4179 }
4180 }
4181 }
4182 // We currently use single-line and auto-height editors in UI contexts,
4183 // so we don't want to scale everything with the buffer font size, as it
4184 // ends up looking off.
4185 EditorMode::SingleLine | EditorMode::AutoHeight { .. } => None,
4186 }
4187 }
4188}
4189
4190impl Element for EditorElement {
4191 type RequestLayoutState = ();
4192 type PrepaintState = EditorLayout;
4193
4194 fn id(&self) -> Option<ElementId> {
4195 None
4196 }
4197
4198 fn request_layout(
4199 &mut self,
4200 _: Option<&GlobalElementId>,
4201 cx: &mut WindowContext,
4202 ) -> (gpui::LayoutId, ()) {
4203 let rem_size = self.rem_size(cx);
4204 cx.with_rem_size(rem_size, |cx| {
4205 self.editor.update(cx, |editor, cx| {
4206 editor.set_style(self.style.clone(), cx);
4207
4208 let layout_id = match editor.mode {
4209 EditorMode::SingleLine => {
4210 let rem_size = cx.rem_size();
4211 let mut style = Style::default();
4212 style.size.width = relative(1.).into();
4213 style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
4214 cx.request_layout(style, None)
4215 }
4216 EditorMode::AutoHeight { max_lines } => {
4217 let editor_handle = cx.view().clone();
4218 let max_line_number_width =
4219 self.max_line_number_width(&editor.snapshot(cx), cx);
4220 cx.request_measured_layout(
4221 Style::default(),
4222 move |known_dimensions, available_space, cx| {
4223 editor_handle
4224 .update(cx, |editor, cx| {
4225 compute_auto_height_layout(
4226 editor,
4227 max_lines,
4228 max_line_number_width,
4229 known_dimensions,
4230 available_space.width,
4231 cx,
4232 )
4233 })
4234 .unwrap_or_default()
4235 },
4236 )
4237 }
4238 EditorMode::Full => {
4239 let mut style = Style::default();
4240 style.size.width = relative(1.).into();
4241 style.size.height = relative(1.).into();
4242 cx.request_layout(style, None)
4243 }
4244 };
4245
4246 (layout_id, ())
4247 })
4248 })
4249 }
4250
4251 fn prepaint(
4252 &mut self,
4253 _: Option<&GlobalElementId>,
4254 bounds: Bounds<Pixels>,
4255 _: &mut Self::RequestLayoutState,
4256 cx: &mut WindowContext,
4257 ) -> Self::PrepaintState {
4258 let text_style = TextStyleRefinement {
4259 font_size: Some(self.style.text.font_size),
4260 line_height: Some(self.style.text.line_height),
4261 ..Default::default()
4262 };
4263 cx.set_view_id(self.editor.entity_id());
4264
4265 let rem_size = self.rem_size(cx);
4266 cx.with_rem_size(rem_size, |cx| {
4267 cx.with_text_style(Some(text_style), |cx| {
4268 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
4269 let mut snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
4270 let style = self.style.clone();
4271
4272 let font_id = cx.text_system().resolve_font(&style.text.font());
4273 let font_size = style.text.font_size.to_pixels(cx.rem_size());
4274 let line_height = style.text.line_height_in_pixels(cx.rem_size());
4275 let em_width = cx
4276 .text_system()
4277 .typographic_bounds(font_id, font_size, 'm')
4278 .unwrap()
4279 .size
4280 .width;
4281 let em_advance = cx
4282 .text_system()
4283 .advance(font_id, font_size, 'm')
4284 .unwrap()
4285 .width;
4286
4287 let gutter_dimensions = snapshot.gutter_dimensions(
4288 font_id,
4289 font_size,
4290 em_width,
4291 self.max_line_number_width(&snapshot, cx),
4292 cx,
4293 );
4294 let text_width = bounds.size.width - gutter_dimensions.width;
4295
4296 let right_margin = if snapshot.mode == EditorMode::Full {
4297 EditorElement::SCROLLBAR_WIDTH
4298 } else {
4299 px(0.)
4300 };
4301 let overscroll = size(em_width + right_margin, px(0.));
4302
4303 snapshot = self.editor.update(cx, |editor, cx| {
4304 editor.last_bounds = Some(bounds);
4305 editor.gutter_dimensions = gutter_dimensions;
4306 editor.set_visible_line_count(bounds.size.height / line_height, cx);
4307
4308 let editor_width =
4309 text_width - gutter_dimensions.margin - overscroll.width - em_width;
4310 let wrap_width = match editor.soft_wrap_mode(cx) {
4311 SoftWrap::None => None,
4312 SoftWrap::PreferLine => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
4313 SoftWrap::EditorWidth => Some(editor_width),
4314 SoftWrap::Column(column) => {
4315 Some(editor_width.min(column as f32 * em_advance))
4316 }
4317 };
4318
4319 if editor.set_wrap_width(wrap_width, cx) {
4320 editor.snapshot(cx)
4321 } else {
4322 snapshot
4323 }
4324 });
4325
4326 let wrap_guides = self
4327 .editor
4328 .read(cx)
4329 .wrap_guides(cx)
4330 .iter()
4331 .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
4332 .collect::<SmallVec<[_; 2]>>();
4333
4334 let hitbox = cx.insert_hitbox(bounds, false);
4335 let gutter_hitbox = cx.insert_hitbox(
4336 Bounds {
4337 origin: bounds.origin,
4338 size: size(gutter_dimensions.width, bounds.size.height),
4339 },
4340 false,
4341 );
4342 let text_hitbox = cx.insert_hitbox(
4343 Bounds {
4344 origin: gutter_hitbox.upper_right(),
4345 size: size(text_width, bounds.size.height),
4346 },
4347 false,
4348 );
4349 // Offset the content_bounds from the text_bounds by the gutter margin (which
4350 // is roughly half a character wide) to make hit testing work more like how we want.
4351 let content_origin =
4352 text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO);
4353
4354 let mut autoscroll_containing_element = false;
4355 let mut autoscroll_horizontally = false;
4356 self.editor.update(cx, |editor, cx| {
4357 autoscroll_containing_element =
4358 editor.autoscroll_requested() || editor.has_pending_selection();
4359 autoscroll_horizontally =
4360 editor.autoscroll_vertically(bounds, line_height, cx);
4361 snapshot = editor.snapshot(cx);
4362 });
4363
4364 let mut scroll_position = snapshot.scroll_position();
4365 // The scroll position is a fractional point, the whole number of which represents
4366 // the top of the window in terms of display rows.
4367 let start_row = DisplayRow(scroll_position.y as u32);
4368 let height_in_lines = bounds.size.height / line_height;
4369 let max_row = snapshot.max_point().row();
4370 let end_row = cmp::min(
4371 (scroll_position.y + height_in_lines).ceil() as u32,
4372 max_row.next_row().0,
4373 );
4374 let end_row = DisplayRow(end_row);
4375
4376 let buffer_rows = snapshot
4377 .buffer_rows(start_row)
4378 .take((start_row..end_row).len())
4379 .collect::<Vec<_>>();
4380
4381 let start_anchor = if start_row == Default::default() {
4382 Anchor::min()
4383 } else {
4384 snapshot.buffer_snapshot.anchor_before(
4385 DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
4386 )
4387 };
4388 let end_anchor = if end_row > max_row {
4389 Anchor::max()
4390 } else {
4391 snapshot.buffer_snapshot.anchor_before(
4392 DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
4393 )
4394 };
4395
4396 let highlighted_rows = self
4397 .editor
4398 .update(cx, |editor, cx| editor.highlighted_display_rows(cx));
4399 let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
4400 start_anchor..end_anchor,
4401 &snapshot.display_snapshot,
4402 cx.theme().colors(),
4403 );
4404
4405 let redacted_ranges = self.editor.read(cx).redacted_ranges(
4406 start_anchor..end_anchor,
4407 &snapshot.display_snapshot,
4408 cx,
4409 );
4410
4411 let (selections, active_rows, newest_selection_head) = self.layout_selections(
4412 start_anchor,
4413 end_anchor,
4414 &snapshot,
4415 start_row,
4416 end_row,
4417 cx,
4418 );
4419
4420 let line_numbers = self.layout_line_numbers(
4421 start_row..end_row,
4422 buffer_rows.iter().copied(),
4423 &active_rows,
4424 newest_selection_head,
4425 &snapshot,
4426 cx,
4427 );
4428
4429 let mut gutter_fold_toggles =
4430 cx.with_element_namespace("gutter_fold_toggles", |cx| {
4431 self.layout_gutter_fold_toggles(
4432 start_row..end_row,
4433 buffer_rows.iter().copied(),
4434 &active_rows,
4435 &snapshot,
4436 cx,
4437 )
4438 });
4439 let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
4440 self.layout_flap_trailers(buffer_rows.iter().copied(), &snapshot, cx)
4441 });
4442
4443 let display_hunks = self.layout_git_gutters(
4444 line_height,
4445 &gutter_hitbox,
4446 start_row..end_row,
4447 &snapshot,
4448 cx,
4449 );
4450
4451 let mut max_visible_line_width = Pixels::ZERO;
4452 let mut line_layouts =
4453 self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
4454 for line_with_invisibles in &line_layouts {
4455 if line_with_invisibles.width > max_visible_line_width {
4456 max_visible_line_width = line_with_invisibles.width;
4457 }
4458 }
4459
4460 let longest_line_width =
4461 layout_line(snapshot.longest_row(), &snapshot, &style, cx).width;
4462 let mut scroll_width =
4463 longest_line_width.max(max_visible_line_width) + overscroll.width;
4464
4465 let mut blocks = cx.with_element_namespace("blocks", |cx| {
4466 self.build_blocks(
4467 start_row..end_row,
4468 &snapshot,
4469 &hitbox,
4470 &text_hitbox,
4471 &mut scroll_width,
4472 &gutter_dimensions,
4473 em_width,
4474 gutter_dimensions.width + gutter_dimensions.margin,
4475 line_height,
4476 &line_layouts,
4477 cx,
4478 )
4479 });
4480
4481 let scroll_pixel_position = point(
4482 scroll_position.x * em_width,
4483 scroll_position.y * line_height,
4484 );
4485
4486 let start_buffer_row =
4487 MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
4488 let end_buffer_row =
4489 MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
4490
4491 let indent_guides = self.layout_indent_guides(
4492 content_origin,
4493 text_hitbox.origin,
4494 start_buffer_row..end_buffer_row,
4495 scroll_pixel_position,
4496 line_height,
4497 &snapshot,
4498 cx,
4499 );
4500
4501 let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
4502 self.prepaint_flap_trailers(
4503 flap_trailers,
4504 &line_layouts,
4505 line_height,
4506 content_origin,
4507 scroll_pixel_position,
4508 em_width,
4509 cx,
4510 )
4511 });
4512
4513 let mut inline_blame = None;
4514 if let Some(newest_selection_head) = newest_selection_head {
4515 let display_row = newest_selection_head.row();
4516 if (start_row..end_row).contains(&display_row) {
4517 let line_ix = display_row.minus(start_row) as usize;
4518 let line_layout = &line_layouts[line_ix];
4519 let flap_trailer_layout = flap_trailers[line_ix].as_ref();
4520 inline_blame = self.layout_inline_blame(
4521 display_row,
4522 &snapshot.display_snapshot,
4523 line_layout,
4524 flap_trailer_layout,
4525 em_width,
4526 content_origin,
4527 scroll_pixel_position,
4528 line_height,
4529 cx,
4530 );
4531 }
4532 }
4533
4534 let blamed_display_rows = self.layout_blame_entries(
4535 buffer_rows.into_iter(),
4536 em_width,
4537 scroll_position,
4538 line_height,
4539 &gutter_hitbox,
4540 gutter_dimensions.git_blame_entries_width,
4541 cx,
4542 );
4543
4544 let scroll_max = point(
4545 ((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
4546 max_row.as_f32(),
4547 );
4548
4549 self.editor.update(cx, |editor, cx| {
4550 let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
4551
4552 let autoscrolled = if autoscroll_horizontally {
4553 editor.autoscroll_horizontally(
4554 start_row,
4555 text_hitbox.size.width,
4556 scroll_width,
4557 em_width,
4558 &line_layouts,
4559 cx,
4560 )
4561 } else {
4562 false
4563 };
4564
4565 if clamped || autoscrolled {
4566 snapshot = editor.snapshot(cx);
4567 scroll_position = snapshot.scroll_position();
4568 }
4569 });
4570
4571 let line_elements = self.prepaint_lines(
4572 start_row,
4573 &mut line_layouts,
4574 line_height,
4575 scroll_pixel_position,
4576 content_origin,
4577 cx,
4578 );
4579
4580 cx.with_element_namespace("blocks", |cx| {
4581 self.layout_blocks(
4582 &mut blocks,
4583 &hitbox,
4584 line_height,
4585 scroll_pixel_position,
4586 cx,
4587 );
4588 });
4589
4590 let cursors = self.collect_cursors(&snapshot, cx);
4591 let visible_row_range = start_row..end_row;
4592 let non_visible_cursors = cursors
4593 .iter()
4594 .any(move |c| !visible_row_range.contains(&c.0.row()));
4595
4596 let visible_cursors = self.layout_visible_cursors(
4597 &snapshot,
4598 &selections,
4599 start_row..end_row,
4600 &line_layouts,
4601 &text_hitbox,
4602 content_origin,
4603 scroll_position,
4604 scroll_pixel_position,
4605 line_height,
4606 em_width,
4607 autoscroll_containing_element,
4608 cx,
4609 );
4610
4611 let scrollbar_layout = self.layout_scrollbar(
4612 &snapshot,
4613 bounds,
4614 scroll_position,
4615 height_in_lines,
4616 non_visible_cursors,
4617 cx,
4618 );
4619
4620 let gutter_settings = EditorSettings::get_global(cx).gutter;
4621
4622 let mut _context_menu_visible = false;
4623 let mut code_actions_indicator = None;
4624 if let Some(newest_selection_head) = newest_selection_head {
4625 if (start_row..end_row).contains(&newest_selection_head.row()) {
4626 _context_menu_visible = self.layout_context_menu(
4627 line_height,
4628 &hitbox,
4629 &text_hitbox,
4630 content_origin,
4631 start_row,
4632 scroll_pixel_position,
4633 &line_layouts,
4634 newest_selection_head,
4635 gutter_dimensions.width - gutter_dimensions.left_padding,
4636 cx,
4637 );
4638
4639 let show_code_actions = snapshot
4640 .show_code_actions
4641 .unwrap_or_else(|| gutter_settings.code_actions);
4642 if show_code_actions {
4643 let newest_selection_point =
4644 newest_selection_head.to_point(&snapshot.display_snapshot);
4645 let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
4646 MultiBufferRow(newest_selection_point.row),
4647 );
4648 if let Some((buffer, range)) = buffer {
4649 let buffer_id = buffer.remote_id();
4650 let row = range.start.row;
4651 let has_test_indicator =
4652 self.editor.read(cx).tasks.contains_key(&(buffer_id, row));
4653
4654 if !has_test_indicator {
4655 code_actions_indicator = self
4656 .layout_code_actions_indicator(
4657 line_height,
4658 newest_selection_head,
4659 scroll_pixel_position,
4660 &gutter_dimensions,
4661 &gutter_hitbox,
4662 cx,
4663 );
4664 }
4665 }
4666 }
4667 }
4668 }
4669
4670 let test_indicators = self.layout_run_indicators(
4671 line_height,
4672 scroll_pixel_position,
4673 &gutter_dimensions,
4674 &gutter_hitbox,
4675 &snapshot,
4676 cx,
4677 );
4678
4679 if !cx.has_active_drag() {
4680 self.layout_hover_popovers(
4681 &snapshot,
4682 &hitbox,
4683 &text_hitbox,
4684 start_row..end_row,
4685 content_origin,
4686 scroll_pixel_position,
4687 &line_layouts,
4688 line_height,
4689 em_width,
4690 cx,
4691 );
4692 }
4693
4694 let mouse_context_menu = self.layout_mouse_context_menu(cx);
4695
4696 cx.with_element_namespace("gutter_fold_toggles", |cx| {
4697 self.prepaint_gutter_fold_toggles(
4698 &mut gutter_fold_toggles,
4699 line_height,
4700 &gutter_dimensions,
4701 gutter_settings,
4702 scroll_pixel_position,
4703 &gutter_hitbox,
4704 cx,
4705 )
4706 });
4707
4708 let invisible_symbol_font_size = font_size / 2.;
4709 let tab_invisible = cx
4710 .text_system()
4711 .shape_line(
4712 "→".into(),
4713 invisible_symbol_font_size,
4714 &[TextRun {
4715 len: "→".len(),
4716 font: self.style.text.font(),
4717 color: cx.theme().colors().editor_invisible,
4718 background_color: None,
4719 underline: None,
4720 strikethrough: None,
4721 }],
4722 )
4723 .unwrap();
4724 let space_invisible = cx
4725 .text_system()
4726 .shape_line(
4727 "•".into(),
4728 invisible_symbol_font_size,
4729 &[TextRun {
4730 len: "•".len(),
4731 font: self.style.text.font(),
4732 color: cx.theme().colors().editor_invisible,
4733 background_color: None,
4734 underline: None,
4735 strikethrough: None,
4736 }],
4737 )
4738 .unwrap();
4739
4740 EditorLayout {
4741 mode: snapshot.mode,
4742 position_map: Arc::new(PositionMap {
4743 size: bounds.size,
4744 scroll_pixel_position,
4745 scroll_max,
4746 line_layouts,
4747 line_height,
4748 em_width,
4749 em_advance,
4750 snapshot,
4751 }),
4752 visible_display_row_range: start_row..end_row,
4753 wrap_guides,
4754 indent_guides,
4755 hitbox,
4756 text_hitbox,
4757 gutter_hitbox,
4758 gutter_dimensions,
4759 content_origin,
4760 scrollbar_layout,
4761 active_rows,
4762 highlighted_rows,
4763 highlighted_ranges,
4764 redacted_ranges,
4765 line_elements,
4766 line_numbers,
4767 display_hunks,
4768 blamed_display_rows,
4769 inline_blame,
4770 blocks,
4771 cursors,
4772 visible_cursors,
4773 selections,
4774 mouse_context_menu,
4775 test_indicators,
4776 code_actions_indicator,
4777 gutter_fold_toggles,
4778 flap_trailers,
4779 tab_invisible,
4780 space_invisible,
4781 }
4782 })
4783 })
4784 })
4785 }
4786
4787 fn paint(
4788 &mut self,
4789 _: Option<&GlobalElementId>,
4790 bounds: Bounds<gpui::Pixels>,
4791 _: &mut Self::RequestLayoutState,
4792 layout: &mut Self::PrepaintState,
4793 cx: &mut WindowContext,
4794 ) {
4795 let focus_handle = self.editor.focus_handle(cx);
4796 let key_context = self.editor.read(cx).key_context(cx);
4797 cx.set_focus_handle(&focus_handle);
4798 cx.set_key_context(key_context);
4799 cx.handle_input(
4800 &focus_handle,
4801 ElementInputHandler::new(bounds, self.editor.clone()),
4802 );
4803 self.register_actions(cx);
4804 self.register_key_listeners(cx, layout);
4805
4806 let text_style = TextStyleRefinement {
4807 font_size: Some(self.style.text.font_size),
4808 line_height: Some(self.style.text.line_height),
4809 ..Default::default()
4810 };
4811 let mouse_position = cx.mouse_position();
4812 let hovered_hunk = layout
4813 .display_hunks
4814 .iter()
4815 .find_map(|(hunk, hunk_hitbox)| match hunk {
4816 DisplayDiffHunk::Folded { .. } => None,
4817 DisplayDiffHunk::Unfolded {
4818 diff_base_byte_range,
4819 multi_buffer_range,
4820 status,
4821 ..
4822 } => {
4823 if hunk_hitbox
4824 .as_ref()
4825 .map(|hitbox| hitbox.contains(&mouse_position))
4826 .unwrap_or(false)
4827 {
4828 Some(HunkToExpand {
4829 status: *status,
4830 multi_buffer_range: multi_buffer_range.clone(),
4831 diff_base_byte_range: diff_base_byte_range.clone(),
4832 })
4833 } else {
4834 None
4835 }
4836 }
4837 });
4838 let rem_size = self.rem_size(cx);
4839 cx.with_rem_size(rem_size, |cx| {
4840 cx.with_text_style(Some(text_style), |cx| {
4841 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
4842 self.paint_mouse_listeners(layout, hovered_hunk, cx);
4843 self.paint_background(layout, cx);
4844 self.paint_indent_guides(layout, cx);
4845 if layout.gutter_hitbox.size.width > Pixels::ZERO {
4846 self.paint_gutter(layout, cx)
4847 }
4848
4849 self.paint_text(layout, cx);
4850
4851 if !layout.blocks.is_empty() {
4852 cx.with_element_namespace("blocks", |cx| {
4853 self.paint_blocks(layout, cx);
4854 });
4855 }
4856
4857 self.paint_scrollbar(layout, cx);
4858 self.paint_mouse_context_menu(layout, cx);
4859 });
4860 })
4861 })
4862 }
4863}
4864
4865impl IntoElement for EditorElement {
4866 type Element = Self;
4867
4868 fn into_element(self) -> Self::Element {
4869 self
4870 }
4871}
4872
4873pub struct EditorLayout {
4874 position_map: Arc<PositionMap>,
4875 hitbox: Hitbox,
4876 text_hitbox: Hitbox,
4877 gutter_hitbox: Hitbox,
4878 gutter_dimensions: GutterDimensions,
4879 content_origin: gpui::Point<Pixels>,
4880 scrollbar_layout: Option<ScrollbarLayout>,
4881 mode: EditorMode,
4882 wrap_guides: SmallVec<[(Pixels, bool); 2]>,
4883 indent_guides: Option<Vec<IndentGuideLayout>>,
4884 visible_display_row_range: Range<DisplayRow>,
4885 active_rows: BTreeMap<DisplayRow, bool>,
4886 highlighted_rows: BTreeMap<DisplayRow, Hsla>,
4887 line_elements: SmallVec<[AnyElement; 1]>,
4888 line_numbers: Vec<Option<ShapedLine>>,
4889 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
4890 blamed_display_rows: Option<Vec<AnyElement>>,
4891 inline_blame: Option<AnyElement>,
4892 blocks: Vec<BlockLayout>,
4893 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
4894 redacted_ranges: Vec<Range<DisplayPoint>>,
4895 cursors: Vec<(DisplayPoint, Hsla)>,
4896 visible_cursors: Vec<CursorLayout>,
4897 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
4898 code_actions_indicator: Option<AnyElement>,
4899 test_indicators: Vec<AnyElement>,
4900 gutter_fold_toggles: Vec<Option<AnyElement>>,
4901 flap_trailers: Vec<Option<FlapTrailerLayout>>,
4902 mouse_context_menu: Option<AnyElement>,
4903 tab_invisible: ShapedLine,
4904 space_invisible: ShapedLine,
4905}
4906
4907impl EditorLayout {
4908 fn line_end_overshoot(&self) -> Pixels {
4909 0.15 * self.position_map.line_height
4910 }
4911}
4912
4913struct ColoredRange<T> {
4914 start: T,
4915 end: T,
4916 color: Hsla,
4917}
4918
4919#[derive(Clone)]
4920struct ScrollbarLayout {
4921 hitbox: Hitbox,
4922 visible_row_range: Range<f32>,
4923 visible: bool,
4924 row_height: Pixels,
4925 thumb_height: Pixels,
4926}
4927
4928impl ScrollbarLayout {
4929 const BORDER_WIDTH: Pixels = px(1.0);
4930 const LINE_MARKER_HEIGHT: Pixels = px(2.0);
4931 const MIN_MARKER_HEIGHT: Pixels = px(5.0);
4932 const MIN_THUMB_HEIGHT: Pixels = px(20.0);
4933
4934 fn thumb_bounds(&self) -> Bounds<Pixels> {
4935 let thumb_top = self.y_for_row(self.visible_row_range.start);
4936 let thumb_bottom = thumb_top + self.thumb_height;
4937 Bounds::from_corners(
4938 point(self.hitbox.left(), thumb_top),
4939 point(self.hitbox.right(), thumb_bottom),
4940 )
4941 }
4942
4943 fn y_for_row(&self, row: f32) -> Pixels {
4944 self.hitbox.top() + row * self.row_height
4945 }
4946
4947 fn marker_quads_for_ranges(
4948 &self,
4949 row_ranges: impl IntoIterator<Item = ColoredRange<DisplayRow>>,
4950 column: Option<usize>,
4951 ) -> Vec<PaintQuad> {
4952 struct MinMax {
4953 min: Pixels,
4954 max: Pixels,
4955 }
4956 let (x_range, height_limit) = if let Some(column) = column {
4957 let column_width = px(((self.hitbox.size.width - Self::BORDER_WIDTH).0 / 3.0).floor());
4958 let start = Self::BORDER_WIDTH + (column as f32 * column_width);
4959 let end = start + column_width;
4960 (
4961 Range { start, end },
4962 MinMax {
4963 min: Self::MIN_MARKER_HEIGHT,
4964 max: px(f32::MAX),
4965 },
4966 )
4967 } else {
4968 (
4969 Range {
4970 start: Self::BORDER_WIDTH,
4971 end: self.hitbox.size.width,
4972 },
4973 MinMax {
4974 min: Self::LINE_MARKER_HEIGHT,
4975 max: Self::LINE_MARKER_HEIGHT,
4976 },
4977 )
4978 };
4979
4980 let row_to_y = |row: DisplayRow| row.as_f32() * self.row_height;
4981 let mut pixel_ranges = row_ranges
4982 .into_iter()
4983 .map(|range| {
4984 let start_y = row_to_y(range.start);
4985 let end_y = row_to_y(range.end)
4986 + self.row_height.max(height_limit.min).min(height_limit.max);
4987 ColoredRange {
4988 start: start_y,
4989 end: end_y,
4990 color: range.color,
4991 }
4992 })
4993 .peekable();
4994
4995 let mut quads = Vec::new();
4996 while let Some(mut pixel_range) = pixel_ranges.next() {
4997 while let Some(next_pixel_range) = pixel_ranges.peek() {
4998 if pixel_range.end >= next_pixel_range.start - px(1.0)
4999 && pixel_range.color == next_pixel_range.color
5000 {
5001 pixel_range.end = next_pixel_range.end.max(pixel_range.end);
5002 pixel_ranges.next();
5003 } else {
5004 break;
5005 }
5006 }
5007
5008 let bounds = Bounds::from_corners(
5009 point(x_range.start, pixel_range.start),
5010 point(x_range.end, pixel_range.end),
5011 );
5012 quads.push(quad(
5013 bounds,
5014 Corners::default(),
5015 pixel_range.color,
5016 Edges::default(),
5017 Hsla::transparent_black(),
5018 ));
5019 }
5020
5021 quads
5022 }
5023}
5024
5025struct FlapTrailerLayout {
5026 element: AnyElement,
5027 bounds: Bounds<Pixels>,
5028}
5029
5030struct PositionMap {
5031 size: Size<Pixels>,
5032 line_height: Pixels,
5033 scroll_pixel_position: gpui::Point<Pixels>,
5034 scroll_max: gpui::Point<f32>,
5035 em_width: Pixels,
5036 em_advance: Pixels,
5037 line_layouts: Vec<LineWithInvisibles>,
5038 snapshot: EditorSnapshot,
5039}
5040
5041#[derive(Debug, Copy, Clone)]
5042pub struct PointForPosition {
5043 pub previous_valid: DisplayPoint,
5044 pub next_valid: DisplayPoint,
5045 pub exact_unclipped: DisplayPoint,
5046 pub column_overshoot_after_line_end: u32,
5047}
5048
5049impl PointForPosition {
5050 pub fn as_valid(&self) -> Option<DisplayPoint> {
5051 if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
5052 Some(self.previous_valid)
5053 } else {
5054 None
5055 }
5056 }
5057}
5058
5059impl PositionMap {
5060 fn point_for_position(
5061 &self,
5062 text_bounds: Bounds<Pixels>,
5063 position: gpui::Point<Pixels>,
5064 ) -> PointForPosition {
5065 let scroll_position = self.snapshot.scroll_position();
5066 let position = position - text_bounds.origin;
5067 let y = position.y.max(px(0.)).min(self.size.height);
5068 let x = position.x + (scroll_position.x * self.em_width);
5069 let row = ((y / self.line_height) + scroll_position.y) as u32;
5070
5071 let (column, x_overshoot_after_line_end) = if let Some(line) = self
5072 .line_layouts
5073 .get(row as usize - scroll_position.y as usize)
5074 {
5075 if let Some(ix) = line.index_for_x(x) {
5076 (ix as u32, px(0.))
5077 } else {
5078 (line.len as u32, px(0.).max(x - line.width))
5079 }
5080 } else {
5081 (0, x)
5082 };
5083
5084 let mut exact_unclipped = DisplayPoint::new(DisplayRow(row), column);
5085 let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
5086 let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
5087
5088 let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
5089 *exact_unclipped.column_mut() += column_overshoot_after_line_end;
5090 PointForPosition {
5091 previous_valid,
5092 next_valid,
5093 exact_unclipped,
5094 column_overshoot_after_line_end,
5095 }
5096 }
5097}
5098
5099struct BlockLayout {
5100 row: DisplayRow,
5101 element: AnyElement,
5102 available_space: Size<AvailableSpace>,
5103 style: BlockStyle,
5104}
5105
5106fn layout_line(
5107 row: DisplayRow,
5108 snapshot: &EditorSnapshot,
5109 style: &EditorStyle,
5110 cx: &mut WindowContext,
5111) -> LineWithInvisibles {
5112 let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
5113 LineWithInvisibles::from_chunks(chunks, &style.text, MAX_LINE_LEN, 1, &[], snapshot.mode, cx)
5114 .pop()
5115 .unwrap()
5116}
5117
5118#[derive(Debug)]
5119pub struct IndentGuideLayout {
5120 origin: gpui::Point<Pixels>,
5121 length: Pixels,
5122 single_indent_width: Pixels,
5123 depth: u32,
5124 active: bool,
5125}
5126
5127pub struct CursorLayout {
5128 origin: gpui::Point<Pixels>,
5129 block_width: Pixels,
5130 line_height: Pixels,
5131 color: Hsla,
5132 shape: CursorShape,
5133 block_text: Option<ShapedLine>,
5134 cursor_name: Option<AnyElement>,
5135}
5136
5137#[derive(Debug)]
5138pub struct CursorName {
5139 string: SharedString,
5140 color: Hsla,
5141 is_top_row: bool,
5142}
5143
5144impl CursorLayout {
5145 pub fn new(
5146 origin: gpui::Point<Pixels>,
5147 block_width: Pixels,
5148 line_height: Pixels,
5149 color: Hsla,
5150 shape: CursorShape,
5151 block_text: Option<ShapedLine>,
5152 ) -> CursorLayout {
5153 CursorLayout {
5154 origin,
5155 block_width,
5156 line_height,
5157 color,
5158 shape,
5159 block_text,
5160 cursor_name: None,
5161 }
5162 }
5163
5164 pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
5165 Bounds {
5166 origin: self.origin + origin,
5167 size: size(self.block_width, self.line_height),
5168 }
5169 }
5170
5171 fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
5172 match self.shape {
5173 CursorShape::Bar => Bounds {
5174 origin: self.origin + origin,
5175 size: size(px(2.0), self.line_height),
5176 },
5177 CursorShape::Block | CursorShape::Hollow => Bounds {
5178 origin: self.origin + origin,
5179 size: size(self.block_width, self.line_height),
5180 },
5181 CursorShape::Underscore => Bounds {
5182 origin: self.origin
5183 + origin
5184 + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)),
5185 size: size(self.block_width, px(2.0)),
5186 },
5187 }
5188 }
5189
5190 pub fn layout(
5191 &mut self,
5192 origin: gpui::Point<Pixels>,
5193 cursor_name: Option<CursorName>,
5194 cx: &mut WindowContext,
5195 ) {
5196 if let Some(cursor_name) = cursor_name {
5197 let bounds = self.bounds(origin);
5198 let text_size = self.line_height / 1.5;
5199
5200 let name_origin = if cursor_name.is_top_row {
5201 point(bounds.right() - px(1.), bounds.top())
5202 } else {
5203 point(bounds.left(), bounds.top() - text_size / 2. - px(1.))
5204 };
5205 let mut name_element = div()
5206 .bg(self.color)
5207 .text_size(text_size)
5208 .px_0p5()
5209 .line_height(text_size + px(2.))
5210 .text_color(cursor_name.color)
5211 .child(cursor_name.string.clone())
5212 .into_any_element();
5213
5214 name_element.prepaint_as_root(
5215 name_origin,
5216 size(AvailableSpace::MinContent, AvailableSpace::MinContent),
5217 cx,
5218 );
5219
5220 self.cursor_name = Some(name_element);
5221 }
5222 }
5223
5224 pub fn paint(&mut self, origin: gpui::Point<Pixels>, cx: &mut WindowContext) {
5225 let bounds = self.bounds(origin);
5226
5227 //Draw background or border quad
5228 let cursor = if matches!(self.shape, CursorShape::Hollow) {
5229 outline(bounds, self.color)
5230 } else {
5231 fill(bounds, self.color)
5232 };
5233
5234 if let Some(name) = &mut self.cursor_name {
5235 name.paint(cx);
5236 }
5237
5238 cx.paint_quad(cursor);
5239
5240 if let Some(block_text) = &self.block_text {
5241 block_text
5242 .paint(self.origin + origin, self.line_height, cx)
5243 .log_err();
5244 }
5245 }
5246
5247 pub fn shape(&self) -> CursorShape {
5248 self.shape
5249 }
5250}
5251
5252#[derive(Debug)]
5253pub struct HighlightedRange {
5254 pub start_y: Pixels,
5255 pub line_height: Pixels,
5256 pub lines: Vec<HighlightedRangeLine>,
5257 pub color: Hsla,
5258 pub corner_radius: Pixels,
5259}
5260
5261#[derive(Debug)]
5262pub struct HighlightedRangeLine {
5263 pub start_x: Pixels,
5264 pub end_x: Pixels,
5265}
5266
5267impl HighlightedRange {
5268 pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
5269 if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
5270 self.paint_lines(self.start_y, &self.lines[0..1], bounds, cx);
5271 self.paint_lines(
5272 self.start_y + self.line_height,
5273 &self.lines[1..],
5274 bounds,
5275 cx,
5276 );
5277 } else {
5278 self.paint_lines(self.start_y, &self.lines, bounds, cx);
5279 }
5280 }
5281
5282 fn paint_lines(
5283 &self,
5284 start_y: Pixels,
5285 lines: &[HighlightedRangeLine],
5286 _bounds: Bounds<Pixels>,
5287 cx: &mut WindowContext,
5288 ) {
5289 if lines.is_empty() {
5290 return;
5291 }
5292
5293 let first_line = lines.first().unwrap();
5294 let last_line = lines.last().unwrap();
5295
5296 let first_top_left = point(first_line.start_x, start_y);
5297 let first_top_right = point(first_line.end_x, start_y);
5298
5299 let curve_height = point(Pixels::ZERO, self.corner_radius);
5300 let curve_width = |start_x: Pixels, end_x: Pixels| {
5301 let max = (end_x - start_x) / 2.;
5302 let width = if max < self.corner_radius {
5303 max
5304 } else {
5305 self.corner_radius
5306 };
5307
5308 point(width, Pixels::ZERO)
5309 };
5310
5311 let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
5312 let mut path = gpui::Path::new(first_top_right - top_curve_width);
5313 path.curve_to(first_top_right + curve_height, first_top_right);
5314
5315 let mut iter = lines.iter().enumerate().peekable();
5316 while let Some((ix, line)) = iter.next() {
5317 let bottom_right = point(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
5318
5319 if let Some((_, next_line)) = iter.peek() {
5320 let next_top_right = point(next_line.end_x, bottom_right.y);
5321
5322 match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() {
5323 Ordering::Equal => {
5324 path.line_to(bottom_right);
5325 }
5326 Ordering::Less => {
5327 let curve_width = curve_width(next_top_right.x, bottom_right.x);
5328 path.line_to(bottom_right - curve_height);
5329 if self.corner_radius > Pixels::ZERO {
5330 path.curve_to(bottom_right - curve_width, bottom_right);
5331 }
5332 path.line_to(next_top_right + curve_width);
5333 if self.corner_radius > Pixels::ZERO {
5334 path.curve_to(next_top_right + curve_height, next_top_right);
5335 }
5336 }
5337 Ordering::Greater => {
5338 let curve_width = curve_width(bottom_right.x, next_top_right.x);
5339 path.line_to(bottom_right - curve_height);
5340 if self.corner_radius > Pixels::ZERO {
5341 path.curve_to(bottom_right + curve_width, bottom_right);
5342 }
5343 path.line_to(next_top_right - curve_width);
5344 if self.corner_radius > Pixels::ZERO {
5345 path.curve_to(next_top_right + curve_height, next_top_right);
5346 }
5347 }
5348 }
5349 } else {
5350 let curve_width = curve_width(line.start_x, line.end_x);
5351 path.line_to(bottom_right - curve_height);
5352 if self.corner_radius > Pixels::ZERO {
5353 path.curve_to(bottom_right - curve_width, bottom_right);
5354 }
5355
5356 let bottom_left = point(line.start_x, bottom_right.y);
5357 path.line_to(bottom_left + curve_width);
5358 if self.corner_radius > Pixels::ZERO {
5359 path.curve_to(bottom_left - curve_height, bottom_left);
5360 }
5361 }
5362 }
5363
5364 if first_line.start_x > last_line.start_x {
5365 let curve_width = curve_width(last_line.start_x, first_line.start_x);
5366 let second_top_left = point(last_line.start_x, start_y + self.line_height);
5367 path.line_to(second_top_left + curve_height);
5368 if self.corner_radius > Pixels::ZERO {
5369 path.curve_to(second_top_left + curve_width, second_top_left);
5370 }
5371 let first_bottom_left = point(first_line.start_x, second_top_left.y);
5372 path.line_to(first_bottom_left - curve_width);
5373 if self.corner_radius > Pixels::ZERO {
5374 path.curve_to(first_bottom_left - curve_height, first_bottom_left);
5375 }
5376 }
5377
5378 path.line_to(first_top_left + curve_height);
5379 if self.corner_radius > Pixels::ZERO {
5380 path.curve_to(first_top_left + top_curve_width, first_top_left);
5381 }
5382 path.line_to(first_top_right - top_curve_width);
5383
5384 cx.paint_path(path, self.color);
5385 }
5386}
5387
5388pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
5389 (delta.pow(1.5) / 100.0).into()
5390}
5391
5392fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
5393 (delta.pow(1.2) / 300.0).into()
5394}
5395
5396#[cfg(test)]
5397mod tests {
5398 use super::*;
5399 use crate::{
5400 display_map::{BlockDisposition, BlockProperties},
5401 editor_tests::{init_test, update_test_language_settings},
5402 Editor, MultiBuffer,
5403 };
5404 use gpui::{TestAppContext, VisualTestContext};
5405 use language::language_settings;
5406 use log::info;
5407 use std::num::NonZeroU32;
5408 use ui::Context;
5409 use util::test::sample_text;
5410
5411 #[gpui::test]
5412 fn test_shape_line_numbers(cx: &mut TestAppContext) {
5413 init_test(cx, |_| {});
5414 let window = cx.add_window(|cx| {
5415 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
5416 Editor::new(EditorMode::Full, buffer, None, cx)
5417 });
5418
5419 let editor = window.root(cx).unwrap();
5420 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
5421 let element = EditorElement::new(&editor, style);
5422 let snapshot = window.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
5423
5424 let layouts = cx
5425 .update_window(*window, |_, cx| {
5426 element.layout_line_numbers(
5427 DisplayRow(0)..DisplayRow(6),
5428 (0..6).map(MultiBufferRow).map(Some),
5429 &Default::default(),
5430 Some(DisplayPoint::new(DisplayRow(0), 0)),
5431 &snapshot,
5432 cx,
5433 )
5434 })
5435 .unwrap();
5436 assert_eq!(layouts.len(), 6);
5437
5438 let relative_rows = window
5439 .update(cx, |editor, cx| {
5440 let snapshot = editor.snapshot(cx);
5441 element.calculate_relative_line_numbers(
5442 &snapshot,
5443 &(DisplayRow(0)..DisplayRow(6)),
5444 Some(DisplayRow(3)),
5445 )
5446 })
5447 .unwrap();
5448 assert_eq!(relative_rows[&DisplayRow(0)], 3);
5449 assert_eq!(relative_rows[&DisplayRow(1)], 2);
5450 assert_eq!(relative_rows[&DisplayRow(2)], 1);
5451 // current line has no relative number
5452 assert_eq!(relative_rows[&DisplayRow(4)], 1);
5453 assert_eq!(relative_rows[&DisplayRow(5)], 2);
5454
5455 // works if cursor is before screen
5456 let relative_rows = window
5457 .update(cx, |editor, cx| {
5458 let snapshot = editor.snapshot(cx);
5459 element.calculate_relative_line_numbers(
5460 &snapshot,
5461 &(DisplayRow(3)..DisplayRow(6)),
5462 Some(DisplayRow(1)),
5463 )
5464 })
5465 .unwrap();
5466 assert_eq!(relative_rows.len(), 3);
5467 assert_eq!(relative_rows[&DisplayRow(3)], 2);
5468 assert_eq!(relative_rows[&DisplayRow(4)], 3);
5469 assert_eq!(relative_rows[&DisplayRow(5)], 4);
5470
5471 // works if cursor is after screen
5472 let relative_rows = window
5473 .update(cx, |editor, cx| {
5474 let snapshot = editor.snapshot(cx);
5475 element.calculate_relative_line_numbers(
5476 &snapshot,
5477 &(DisplayRow(0)..DisplayRow(3)),
5478 Some(DisplayRow(6)),
5479 )
5480 })
5481 .unwrap();
5482 assert_eq!(relative_rows.len(), 3);
5483 assert_eq!(relative_rows[&DisplayRow(0)], 5);
5484 assert_eq!(relative_rows[&DisplayRow(1)], 4);
5485 assert_eq!(relative_rows[&DisplayRow(2)], 3);
5486 }
5487
5488 #[gpui::test]
5489 async fn test_vim_visual_selections(cx: &mut TestAppContext) {
5490 init_test(cx, |_| {});
5491
5492 let window = cx.add_window(|cx| {
5493 let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
5494 Editor::new(EditorMode::Full, buffer, None, cx)
5495 });
5496 let cx = &mut VisualTestContext::from_window(*window, cx);
5497 let editor = window.root(cx).unwrap();
5498 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
5499
5500 window
5501 .update(cx, |editor, cx| {
5502 editor.cursor_shape = CursorShape::Block;
5503 editor.change_selections(None, cx, |s| {
5504 s.select_ranges([
5505 Point::new(0, 0)..Point::new(1, 0),
5506 Point::new(3, 2)..Point::new(3, 3),
5507 Point::new(5, 6)..Point::new(6, 0),
5508 ]);
5509 });
5510 })
5511 .unwrap();
5512
5513 let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| {
5514 EditorElement::new(&editor, style)
5515 });
5516
5517 assert_eq!(state.selections.len(), 1);
5518 let local_selections = &state.selections[0].1;
5519 assert_eq!(local_selections.len(), 3);
5520 // moves cursor back one line
5521 assert_eq!(
5522 local_selections[0].head,
5523 DisplayPoint::new(DisplayRow(0), 6)
5524 );
5525 assert_eq!(
5526 local_selections[0].range,
5527 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0)
5528 );
5529
5530 // moves cursor back one column
5531 assert_eq!(
5532 local_selections[1].range,
5533 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 3)
5534 );
5535 assert_eq!(
5536 local_selections[1].head,
5537 DisplayPoint::new(DisplayRow(3), 2)
5538 );
5539
5540 // leaves cursor on the max point
5541 assert_eq!(
5542 local_selections[2].range,
5543 DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(6), 0)
5544 );
5545 assert_eq!(
5546 local_selections[2].head,
5547 DisplayPoint::new(DisplayRow(6), 0)
5548 );
5549
5550 // active lines does not include 1 (even though the range of the selection does)
5551 assert_eq!(
5552 state.active_rows.keys().cloned().collect::<Vec<_>>(),
5553 vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
5554 );
5555
5556 // multi-buffer support
5557 // in DisplayPoint coordinates, this is what we're dealing with:
5558 // 0: [[file
5559 // 1: header]]
5560 // 2: aaaaaa
5561 // 3: bbbbbb
5562 // 4: cccccc
5563 // 5:
5564 // 6: ...
5565 // 7: ffffff
5566 // 8: gggggg
5567 // 9: hhhhhh
5568 // 10:
5569 // 11: [[file
5570 // 12: header]]
5571 // 13: bbbbbb
5572 // 14: cccccc
5573 // 15: dddddd
5574 let window = cx.add_window(|cx| {
5575 let buffer = MultiBuffer::build_multi(
5576 [
5577 (
5578 &(sample_text(8, 6, 'a') + "\n"),
5579 vec![
5580 Point::new(0, 0)..Point::new(3, 0),
5581 Point::new(4, 0)..Point::new(7, 0),
5582 ],
5583 ),
5584 (
5585 &(sample_text(8, 6, 'a') + "\n"),
5586 vec![Point::new(1, 0)..Point::new(3, 0)],
5587 ),
5588 ],
5589 cx,
5590 );
5591 Editor::new(EditorMode::Full, buffer, None, cx)
5592 });
5593 let editor = window.root(cx).unwrap();
5594 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
5595 let _state = window.update(cx, |editor, cx| {
5596 editor.cursor_shape = CursorShape::Block;
5597 editor.change_selections(None, cx, |s| {
5598 s.select_display_ranges([
5599 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0),
5600 DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0),
5601 ]);
5602 });
5603 });
5604
5605 let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| {
5606 EditorElement::new(&editor, style)
5607 });
5608 assert_eq!(state.selections.len(), 1);
5609 let local_selections = &state.selections[0].1;
5610 assert_eq!(local_selections.len(), 2);
5611
5612 // moves cursor on excerpt boundary back a line
5613 // and doesn't allow selection to bleed through
5614 assert_eq!(
5615 local_selections[0].range,
5616 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(6), 0)
5617 );
5618 assert_eq!(
5619 local_selections[0].head,
5620 DisplayPoint::new(DisplayRow(5), 0)
5621 );
5622 // moves cursor on buffer boundary back two lines
5623 // and doesn't allow selection to bleed through
5624 assert_eq!(
5625 local_selections[1].range,
5626 DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(11), 0)
5627 );
5628 assert_eq!(
5629 local_selections[1].head,
5630 DisplayPoint::new(DisplayRow(10), 0)
5631 );
5632 }
5633
5634 #[gpui::test]
5635 fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
5636 init_test(cx, |_| {});
5637
5638 let window = cx.add_window(|cx| {
5639 let buffer = MultiBuffer::build_simple("", cx);
5640 Editor::new(EditorMode::Full, buffer, None, cx)
5641 });
5642 let cx = &mut VisualTestContext::from_window(*window, cx);
5643 let editor = window.root(cx).unwrap();
5644 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
5645 window
5646 .update(cx, |editor, cx| {
5647 editor.set_placeholder_text("hello", cx);
5648 editor.insert_blocks(
5649 [BlockProperties {
5650 style: BlockStyle::Fixed,
5651 disposition: BlockDisposition::Above,
5652 height: 3,
5653 position: Anchor::min(),
5654 render: Box::new(|_| div().into_any()),
5655 }],
5656 None,
5657 cx,
5658 );
5659
5660 // Blur the editor so that it displays placeholder text.
5661 cx.blur();
5662 })
5663 .unwrap();
5664
5665 let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| {
5666 EditorElement::new(&editor, style)
5667 });
5668 assert_eq!(state.position_map.line_layouts.len(), 4);
5669 assert_eq!(
5670 state
5671 .line_numbers
5672 .iter()
5673 .map(Option::is_some)
5674 .collect::<Vec<_>>(),
5675 &[false, false, false, true]
5676 );
5677 }
5678
5679 #[gpui::test]
5680 fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
5681 const TAB_SIZE: u32 = 4;
5682
5683 let input_text = "\t \t|\t| a b";
5684 let expected_invisibles = vec![
5685 Invisible::Tab {
5686 line_start_offset: 0,
5687 },
5688 Invisible::Whitespace {
5689 line_offset: TAB_SIZE as usize,
5690 },
5691 Invisible::Tab {
5692 line_start_offset: TAB_SIZE as usize + 1,
5693 },
5694 Invisible::Tab {
5695 line_start_offset: TAB_SIZE as usize * 2 + 1,
5696 },
5697 Invisible::Whitespace {
5698 line_offset: TAB_SIZE as usize * 3 + 1,
5699 },
5700 Invisible::Whitespace {
5701 line_offset: TAB_SIZE as usize * 3 + 3,
5702 },
5703 ];
5704 assert_eq!(
5705 expected_invisibles.len(),
5706 input_text
5707 .chars()
5708 .filter(|initial_char| initial_char.is_whitespace())
5709 .count(),
5710 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
5711 );
5712
5713 init_test(cx, |s| {
5714 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
5715 s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
5716 });
5717
5718 let actual_invisibles =
5719 collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, px(500.0));
5720
5721 assert_eq!(expected_invisibles, actual_invisibles);
5722 }
5723
5724 #[gpui::test]
5725 fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
5726 init_test(cx, |s| {
5727 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
5728 s.defaults.tab_size = NonZeroU32::new(4);
5729 });
5730
5731 for editor_mode_without_invisibles in [
5732 EditorMode::SingleLine,
5733 EditorMode::AutoHeight { max_lines: 100 },
5734 ] {
5735 let invisibles = collect_invisibles_from_new_editor(
5736 cx,
5737 editor_mode_without_invisibles,
5738 "\t\t\t| | a b",
5739 px(500.0),
5740 );
5741 assert!(invisibles.is_empty(),
5742 "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
5743 }
5744 }
5745
5746 #[gpui::test]
5747 fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
5748 let tab_size = 4;
5749 let input_text = "a\tbcd ".repeat(9);
5750 let repeated_invisibles = [
5751 Invisible::Tab {
5752 line_start_offset: 1,
5753 },
5754 Invisible::Whitespace {
5755 line_offset: tab_size as usize + 3,
5756 },
5757 Invisible::Whitespace {
5758 line_offset: tab_size as usize + 4,
5759 },
5760 Invisible::Whitespace {
5761 line_offset: tab_size as usize + 5,
5762 },
5763 ];
5764 let expected_invisibles = std::iter::once(repeated_invisibles)
5765 .cycle()
5766 .take(9)
5767 .flatten()
5768 .collect::<Vec<_>>();
5769 assert_eq!(
5770 expected_invisibles.len(),
5771 input_text
5772 .chars()
5773 .filter(|initial_char| initial_char.is_whitespace())
5774 .count(),
5775 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
5776 );
5777 info!("Expected invisibles: {expected_invisibles:?}");
5778
5779 init_test(cx, |_| {});
5780
5781 // Put the same string with repeating whitespace pattern into editors of various size,
5782 // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
5783 let resize_step = 10.0;
5784 let mut editor_width = 200.0;
5785 while editor_width <= 1000.0 {
5786 update_test_language_settings(cx, |s| {
5787 s.defaults.tab_size = NonZeroU32::new(tab_size);
5788 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
5789 s.defaults.preferred_line_length = Some(editor_width as u32);
5790 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
5791 });
5792
5793 let actual_invisibles = collect_invisibles_from_new_editor(
5794 cx,
5795 EditorMode::Full,
5796 &input_text,
5797 px(editor_width),
5798 );
5799
5800 // Whatever the editor size is, ensure it has the same invisible kinds in the same order
5801 // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
5802 let mut i = 0;
5803 for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
5804 i = actual_index;
5805 match expected_invisibles.get(i) {
5806 Some(expected_invisible) => match (expected_invisible, actual_invisible) {
5807 (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
5808 | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
5809 _ => {
5810 panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
5811 }
5812 },
5813 None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
5814 }
5815 }
5816 let missing_expected_invisibles = &expected_invisibles[i + 1..];
5817 assert!(
5818 missing_expected_invisibles.is_empty(),
5819 "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
5820 );
5821
5822 editor_width += resize_step;
5823 }
5824 }
5825
5826 fn collect_invisibles_from_new_editor(
5827 cx: &mut TestAppContext,
5828 editor_mode: EditorMode,
5829 input_text: &str,
5830 editor_width: Pixels,
5831 ) -> Vec<Invisible> {
5832 info!(
5833 "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
5834 editor_width.0
5835 );
5836 let window = cx.add_window(|cx| {
5837 let buffer = MultiBuffer::build_simple(&input_text, cx);
5838 Editor::new(editor_mode, buffer, None, cx)
5839 });
5840 let cx = &mut VisualTestContext::from_window(*window, cx);
5841 let editor = window.root(cx).unwrap();
5842 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
5843 window
5844 .update(cx, |editor, cx| {
5845 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
5846 editor.set_wrap_width(Some(editor_width), cx);
5847 })
5848 .unwrap();
5849 let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| {
5850 EditorElement::new(&editor, style)
5851 });
5852 state
5853 .position_map
5854 .line_layouts
5855 .iter()
5856 .flat_map(|line_with_invisibles| &line_with_invisibles.invisibles)
5857 .cloned()
5858 .collect()
5859 }
5860}
5861
5862pub fn register_action<T: Action>(
5863 view: &View<Editor>,
5864 cx: &mut WindowContext,
5865 listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static,
5866) {
5867 let view = view.clone();
5868 cx.on_action(TypeId::of::<T>(), move |action, phase, cx| {
5869 let action = action.downcast_ref().unwrap();
5870 if phase == DispatchPhase::Bubble {
5871 view.update(cx, |editor, cx| {
5872 listener(editor, action, cx);
5873 })
5874 }
5875 })
5876}
5877
5878fn compute_auto_height_layout(
5879 editor: &mut Editor,
5880 max_lines: usize,
5881 max_line_number_width: Pixels,
5882 known_dimensions: Size<Option<Pixels>>,
5883 available_width: AvailableSpace,
5884 cx: &mut ViewContext<Editor>,
5885) -> Option<Size<Pixels>> {
5886 let width = known_dimensions.width.or_else(|| {
5887 if let AvailableSpace::Definite(available_width) = available_width {
5888 Some(available_width)
5889 } else {
5890 None
5891 }
5892 })?;
5893 if let Some(height) = known_dimensions.height {
5894 return Some(size(width, height));
5895 }
5896
5897 let style = editor.style.as_ref().unwrap();
5898 let font_id = cx.text_system().resolve_font(&style.text.font());
5899 let font_size = style.text.font_size.to_pixels(cx.rem_size());
5900 let line_height = style.text.line_height_in_pixels(cx.rem_size());
5901 let em_width = cx
5902 .text_system()
5903 .typographic_bounds(font_id, font_size, 'm')
5904 .unwrap()
5905 .size
5906 .width;
5907
5908 let mut snapshot = editor.snapshot(cx);
5909 let gutter_dimensions =
5910 snapshot.gutter_dimensions(font_id, font_size, em_width, max_line_number_width, cx);
5911
5912 editor.gutter_dimensions = gutter_dimensions;
5913 let text_width = width - gutter_dimensions.width;
5914 let overscroll = size(em_width, px(0.));
5915
5916 let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
5917 if editor.set_wrap_width(Some(editor_width), cx) {
5918 snapshot = editor.snapshot(cx);
5919 }
5920
5921 let scroll_height = Pixels::from(snapshot.max_point().row().next_row().0) * line_height;
5922 let height = scroll_height
5923 .max(line_height)
5924 .min(line_height * max_lines as f32);
5925
5926 Some(size(width, height))
5927}