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