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