1use crate::{
2 code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
3 commit_tooltip::{blame_entry_relative_timestamp, CommitTooltip, ParsedCommitMessage},
4 display_map::{
5 Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
6 },
7 editor_settings::{
8 CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
9 ScrollbarDiagnostics, ShowScrollbar,
10 },
11 git::blame::GitBlame,
12 hover_popover::{
13 self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
14 },
15 items::BufferSearchHighlights,
16 mouse_context_menu::{self, MenuPosition, MouseContextMenu},
17 scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
18 BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
19 DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
20 EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
21 GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
22 InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt,
23 RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt,
24 ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
25 GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
26};
27use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
28use client::ParticipantIndex;
29use collections::{BTreeMap, HashMap, HashSet};
30use file_icons::FileIcons;
31use git::{blame::BlameEntry, Oid};
32use gpui::{
33 anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
34 relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
35 ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
36 Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
37 Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
38 MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
39 ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
40 Subscription, TextRun, TextStyleRefinement, Window,
41};
42use itertools::Itertools;
43use language::{
44 language_settings::{
45 IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
46 ShowWhitespaceSetting,
47 },
48 ChunkRendererContext,
49};
50use lsp::DiagnosticSeverity;
51use multi_buffer::{
52 Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
53 RowInfo, ToOffset,
54};
55use project::project_settings::{GitGutterSetting, ProjectSettings};
56use settings::Settings;
57use smallvec::{smallvec, SmallVec};
58use std::{
59 any::TypeId,
60 borrow::Cow,
61 cmp::{self, Ordering},
62 fmt::{self, Write},
63 iter, mem,
64 ops::{Deref, Range},
65 rc::Rc,
66 sync::Arc,
67};
68use sum_tree::Bias;
69use text::BufferId;
70use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
71use ui::{
72 h_flex, prelude::*, ButtonLike, ButtonStyle, ContextMenu, IconButtonShape, KeyBinding, Tooltip,
73 POPOVER_Y_PADDING,
74};
75use unicode_segmentation::UnicodeSegmentation;
76use util::{debug_panic, RangeExt, ResultExt};
77use workspace::{item::Item, notifications::NotifyTaskExt};
78
79const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
80const MIN_SCROLL_THUMB_SIZE: f32 = 25.;
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83enum DisplayDiffHunk {
84 Folded {
85 display_row: DisplayRow,
86 },
87 Unfolded {
88 diff_base_byte_range: Range<usize>,
89 display_row_range: Range<DisplayRow>,
90 multi_buffer_range: Range<Anchor>,
91 status: DiffHunkStatus,
92 },
93}
94
95struct SelectionLayout {
96 head: DisplayPoint,
97 cursor_shape: CursorShape,
98 is_newest: bool,
99 is_local: bool,
100 range: Range<DisplayPoint>,
101 active_rows: Range<DisplayRow>,
102 user_name: Option<SharedString>,
103}
104
105impl SelectionLayout {
106 fn new<T: ToPoint + ToDisplayPoint + Clone>(
107 selection: Selection<T>,
108 line_mode: bool,
109 cursor_shape: CursorShape,
110 map: &DisplaySnapshot,
111 is_newest: bool,
112 is_local: bool,
113 user_name: Option<SharedString>,
114 ) -> Self {
115 let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
116 let display_selection = point_selection.map(|p| p.to_display_point(map));
117 let mut range = display_selection.range();
118 let mut head = display_selection.head();
119 let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
120 ..map.next_line_boundary(point_selection.end).1.row();
121
122 // vim visual line mode
123 if line_mode {
124 let point_range = map.expand_to_line(point_selection.range());
125 range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
126 }
127
128 // any vim visual mode (including line mode)
129 if (cursor_shape == CursorShape::Block || cursor_shape == CursorShape::Hollow)
130 && !range.is_empty()
131 && !selection.reversed
132 {
133 if head.column() > 0 {
134 head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
135 } else if head.row().0 > 0 && head != map.max_point() {
136 head = map.clip_point(
137 DisplayPoint::new(
138 head.row().previous_row(),
139 map.line_len(head.row().previous_row()),
140 ),
141 Bias::Left,
142 );
143 // updating range.end is a no-op unless you're cursor is
144 // on the newline containing a multi-buffer divider
145 // in which case the clip_point may have moved the head up
146 // an additional row.
147 range.end = DisplayPoint::new(head.row().next_row(), 0);
148 active_rows.end = head.row();
149 }
150 }
151
152 Self {
153 head,
154 cursor_shape,
155 is_newest,
156 is_local,
157 range,
158 active_rows,
159 user_name,
160 }
161 }
162}
163
164pub struct EditorElement {
165 editor: Entity<Editor>,
166 style: EditorStyle,
167}
168
169type DisplayRowDelta = u32;
170
171impl EditorElement {
172 pub(crate) const SCROLLBAR_WIDTH: Pixels = px(15.);
173
174 pub fn new(editor: &Entity<Editor>, style: EditorStyle) -> Self {
175 Self {
176 editor: editor.clone(),
177 style,
178 }
179 }
180
181 fn register_actions(&self, window: &mut Window, cx: &mut App) {
182 let editor = &self.editor;
183 editor.update(cx, |editor, cx| {
184 for action in editor.editor_actions.borrow().values() {
185 (action)(window, cx)
186 }
187 });
188
189 crate::rust_analyzer_ext::apply_related_actions(editor, window, cx);
190 crate::clangd_ext::apply_related_actions(editor, window, cx);
191 register_action(editor, window, Editor::open_context_menu);
192 register_action(editor, window, Editor::move_left);
193 register_action(editor, window, Editor::move_right);
194 register_action(editor, window, Editor::move_down);
195 register_action(editor, window, Editor::move_down_by_lines);
196 register_action(editor, window, Editor::select_down_by_lines);
197 register_action(editor, window, Editor::move_up);
198 register_action(editor, window, Editor::move_up_by_lines);
199 register_action(editor, window, Editor::select_up_by_lines);
200 register_action(editor, window, Editor::select_page_down);
201 register_action(editor, window, Editor::select_page_up);
202 register_action(editor, window, Editor::cancel);
203 register_action(editor, window, Editor::newline);
204 register_action(editor, window, Editor::newline_above);
205 register_action(editor, window, Editor::newline_below);
206 register_action(editor, window, Editor::backspace);
207 register_action(editor, window, Editor::delete);
208 register_action(editor, window, Editor::tab);
209 register_action(editor, window, Editor::tab_prev);
210 register_action(editor, window, Editor::indent);
211 register_action(editor, window, Editor::outdent);
212 register_action(editor, window, Editor::autoindent);
213 register_action(editor, window, Editor::delete_line);
214 register_action(editor, window, Editor::join_lines);
215 register_action(editor, window, Editor::sort_lines_case_sensitive);
216 register_action(editor, window, Editor::sort_lines_case_insensitive);
217 register_action(editor, window, Editor::reverse_lines);
218 register_action(editor, window, Editor::shuffle_lines);
219 register_action(editor, window, Editor::convert_to_upper_case);
220 register_action(editor, window, Editor::convert_to_lower_case);
221 register_action(editor, window, Editor::convert_to_title_case);
222 register_action(editor, window, Editor::convert_to_snake_case);
223 register_action(editor, window, Editor::convert_to_kebab_case);
224 register_action(editor, window, Editor::convert_to_upper_camel_case);
225 register_action(editor, window, Editor::convert_to_lower_camel_case);
226 register_action(editor, window, Editor::convert_to_opposite_case);
227 register_action(editor, window, Editor::delete_to_previous_word_start);
228 register_action(editor, window, Editor::delete_to_previous_subword_start);
229 register_action(editor, window, Editor::delete_to_next_word_end);
230 register_action(editor, window, Editor::delete_to_next_subword_end);
231 register_action(editor, window, Editor::delete_to_beginning_of_line);
232 register_action(editor, window, Editor::delete_to_end_of_line);
233 register_action(editor, window, Editor::cut_to_end_of_line);
234 register_action(editor, window, Editor::duplicate_line_up);
235 register_action(editor, window, Editor::duplicate_line_down);
236 register_action(editor, window, Editor::duplicate_selection);
237 register_action(editor, window, Editor::move_line_up);
238 register_action(editor, window, Editor::move_line_down);
239 register_action(editor, window, Editor::transpose);
240 register_action(editor, window, Editor::rewrap);
241 register_action(editor, window, Editor::cut);
242 register_action(editor, window, Editor::kill_ring_cut);
243 register_action(editor, window, Editor::kill_ring_yank);
244 register_action(editor, window, Editor::copy);
245 register_action(editor, window, Editor::paste);
246 register_action(editor, window, Editor::undo);
247 register_action(editor, window, Editor::redo);
248 register_action(editor, window, Editor::move_page_up);
249 register_action(editor, window, Editor::move_page_down);
250 register_action(editor, window, Editor::next_screen);
251 register_action(editor, window, Editor::scroll_cursor_top);
252 register_action(editor, window, Editor::scroll_cursor_center);
253 register_action(editor, window, Editor::scroll_cursor_bottom);
254 register_action(editor, window, Editor::scroll_cursor_center_top_bottom);
255 register_action(editor, window, |editor, _: &LineDown, window, cx| {
256 editor.scroll_screen(&ScrollAmount::Line(1.), window, cx)
257 });
258 register_action(editor, window, |editor, _: &LineUp, window, cx| {
259 editor.scroll_screen(&ScrollAmount::Line(-1.), window, cx)
260 });
261 register_action(editor, window, |editor, _: &HalfPageDown, window, cx| {
262 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx)
263 });
264 register_action(
265 editor,
266 window,
267 |editor, HandleInput(text): &HandleInput, window, cx| {
268 if text.is_empty() {
269 return;
270 }
271 editor.handle_input(text, window, cx);
272 },
273 );
274 register_action(editor, window, |editor, _: &HalfPageUp, window, cx| {
275 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx)
276 });
277 register_action(editor, window, |editor, _: &PageDown, window, cx| {
278 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx)
279 });
280 register_action(editor, window, |editor, _: &PageUp, window, cx| {
281 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx)
282 });
283 register_action(editor, window, Editor::move_to_previous_word_start);
284 register_action(editor, window, Editor::move_to_previous_subword_start);
285 register_action(editor, window, Editor::move_to_next_word_end);
286 register_action(editor, window, Editor::move_to_next_subword_end);
287 register_action(editor, window, Editor::move_to_beginning_of_line);
288 register_action(editor, window, Editor::move_to_end_of_line);
289 register_action(editor, window, Editor::move_to_start_of_paragraph);
290 register_action(editor, window, Editor::move_to_end_of_paragraph);
291 register_action(editor, window, Editor::move_to_beginning);
292 register_action(editor, window, Editor::move_to_end);
293 register_action(editor, window, Editor::select_up);
294 register_action(editor, window, Editor::select_down);
295 register_action(editor, window, Editor::select_left);
296 register_action(editor, window, Editor::select_right);
297 register_action(editor, window, Editor::select_to_previous_word_start);
298 register_action(editor, window, Editor::select_to_previous_subword_start);
299 register_action(editor, window, Editor::select_to_next_word_end);
300 register_action(editor, window, Editor::select_to_next_subword_end);
301 register_action(editor, window, Editor::select_to_beginning_of_line);
302 register_action(editor, window, Editor::select_to_end_of_line);
303 register_action(editor, window, Editor::select_to_start_of_paragraph);
304 register_action(editor, window, Editor::select_to_end_of_paragraph);
305 register_action(editor, window, Editor::select_to_beginning);
306 register_action(editor, window, Editor::select_to_end);
307 register_action(editor, window, Editor::select_all);
308 register_action(editor, window, |editor, action, window, cx| {
309 editor.select_all_matches(action, window, cx).log_err();
310 });
311 register_action(editor, window, Editor::select_line);
312 register_action(editor, window, Editor::split_selection_into_lines);
313 register_action(editor, window, Editor::add_selection_above);
314 register_action(editor, window, Editor::add_selection_below);
315 register_action(editor, window, |editor, action, window, cx| {
316 editor.select_next(action, window, cx).log_err();
317 });
318 register_action(editor, window, |editor, action, window, cx| {
319 editor.select_previous(action, window, cx).log_err();
320 });
321 register_action(editor, window, Editor::toggle_comments);
322 register_action(editor, window, Editor::select_larger_syntax_node);
323 register_action(editor, window, Editor::select_smaller_syntax_node);
324 register_action(editor, window, Editor::select_enclosing_symbol);
325 register_action(editor, window, Editor::move_to_enclosing_bracket);
326 register_action(editor, window, Editor::undo_selection);
327 register_action(editor, window, Editor::redo_selection);
328 if !editor.read(cx).is_singleton(cx) {
329 register_action(editor, window, Editor::expand_excerpts);
330 register_action(editor, window, Editor::expand_excerpts_up);
331 register_action(editor, window, Editor::expand_excerpts_down);
332 }
333 register_action(editor, window, Editor::go_to_diagnostic);
334 register_action(editor, window, Editor::go_to_prev_diagnostic);
335 register_action(editor, window, Editor::go_to_next_hunk);
336 register_action(editor, window, Editor::go_to_prev_hunk);
337 register_action(editor, window, |editor, action, window, cx| {
338 editor
339 .go_to_definition(action, window, cx)
340 .detach_and_log_err(cx);
341 });
342 register_action(editor, window, |editor, action, window, cx| {
343 editor
344 .go_to_definition_split(action, window, cx)
345 .detach_and_log_err(cx);
346 });
347 register_action(editor, window, |editor, action, window, cx| {
348 editor
349 .go_to_declaration(action, window, cx)
350 .detach_and_log_err(cx);
351 });
352 register_action(editor, window, |editor, action, window, cx| {
353 editor
354 .go_to_declaration_split(action, window, cx)
355 .detach_and_log_err(cx);
356 });
357 register_action(editor, window, |editor, action, window, cx| {
358 editor
359 .go_to_implementation(action, window, cx)
360 .detach_and_log_err(cx);
361 });
362 register_action(editor, window, |editor, action, window, cx| {
363 editor
364 .go_to_implementation_split(action, window, cx)
365 .detach_and_log_err(cx);
366 });
367 register_action(editor, window, |editor, action, window, cx| {
368 editor
369 .go_to_type_definition(action, window, cx)
370 .detach_and_log_err(cx);
371 });
372 register_action(editor, window, |editor, action, window, cx| {
373 editor
374 .go_to_type_definition_split(action, window, cx)
375 .detach_and_log_err(cx);
376 });
377 register_action(editor, window, Editor::open_url);
378 register_action(editor, window, Editor::open_selected_filename);
379 register_action(editor, window, Editor::fold);
380 register_action(editor, window, Editor::fold_at_level);
381 register_action(editor, window, Editor::fold_all);
382 register_action(editor, window, Editor::fold_function_bodies);
383 register_action(editor, window, Editor::fold_at);
384 register_action(editor, window, Editor::fold_recursive);
385 register_action(editor, window, Editor::toggle_fold);
386 register_action(editor, window, Editor::toggle_fold_recursive);
387 register_action(editor, window, Editor::unfold_lines);
388 register_action(editor, window, Editor::unfold_recursive);
389 register_action(editor, window, Editor::unfold_all);
390 register_action(editor, window, Editor::unfold_at);
391 register_action(editor, window, Editor::fold_selected_ranges);
392 register_action(editor, window, Editor::set_mark);
393 register_action(editor, window, Editor::swap_selection_ends);
394 register_action(editor, window, Editor::show_completions);
395 register_action(editor, window, Editor::toggle_code_actions);
396 register_action(editor, window, Editor::open_excerpts);
397 register_action(editor, window, Editor::open_excerpts_in_split);
398 register_action(editor, window, Editor::open_proposed_changes_editor);
399 register_action(editor, window, Editor::toggle_soft_wrap);
400 register_action(editor, window, Editor::toggle_tab_bar);
401 register_action(editor, window, Editor::toggle_line_numbers);
402 register_action(editor, window, Editor::toggle_relative_line_numbers);
403 register_action(editor, window, Editor::toggle_indent_guides);
404 register_action(editor, window, Editor::toggle_inlay_hints);
405 register_action(editor, window, Editor::toggle_inline_completions);
406 register_action(editor, window, hover_popover::hover);
407 register_action(editor, window, Editor::reveal_in_finder);
408 register_action(editor, window, Editor::copy_path);
409 register_action(editor, window, Editor::copy_relative_path);
410 register_action(editor, window, Editor::copy_file_name);
411 register_action(editor, window, Editor::copy_file_name_without_extension);
412 register_action(editor, window, Editor::copy_highlight_json);
413 register_action(editor, window, Editor::copy_permalink_to_line);
414 register_action(editor, window, Editor::open_permalink_to_line);
415 register_action(editor, window, Editor::copy_file_location);
416 register_action(editor, window, Editor::toggle_git_blame);
417 register_action(editor, window, Editor::toggle_git_blame_inline);
418 register_action(editor, window, Editor::toggle_selected_diff_hunks);
419 register_action(editor, window, Editor::toggle_staged_selected_diff_hunks);
420 register_action(editor, window, Editor::stage_and_next);
421 register_action(editor, window, Editor::unstage_and_next);
422 register_action(editor, window, Editor::expand_all_diff_hunks);
423
424 register_action(editor, window, |editor, action, window, cx| {
425 if let Some(task) = editor.format(action, window, cx) {
426 task.detach_and_notify_err(window, cx);
427 } else {
428 cx.propagate();
429 }
430 });
431 register_action(editor, window, |editor, action, window, cx| {
432 if let Some(task) = editor.format_selections(action, window, cx) {
433 task.detach_and_notify_err(window, cx);
434 } else {
435 cx.propagate();
436 }
437 });
438 register_action(editor, window, Editor::restart_language_server);
439 register_action(editor, window, Editor::show_character_palette);
440 register_action(editor, window, |editor, action, window, cx| {
441 if let Some(task) = editor.confirm_completion(action, window, cx) {
442 task.detach_and_notify_err(window, cx);
443 } else {
444 cx.propagate();
445 }
446 });
447 register_action(editor, window, |editor, action, window, cx| {
448 if let Some(task) = editor.compose_completion(action, window, cx) {
449 task.detach_and_notify_err(window, cx);
450 } else {
451 cx.propagate();
452 }
453 });
454 register_action(editor, window, |editor, action, window, cx| {
455 if let Some(task) = editor.confirm_code_action(action, window, cx) {
456 task.detach_and_notify_err(window, cx);
457 } else {
458 cx.propagate();
459 }
460 });
461 register_action(editor, window, |editor, action, window, cx| {
462 if let Some(task) = editor.rename(action, window, cx) {
463 task.detach_and_notify_err(window, cx);
464 } else {
465 cx.propagate();
466 }
467 });
468 register_action(editor, window, |editor, action, window, cx| {
469 if let Some(task) = editor.confirm_rename(action, window, cx) {
470 task.detach_and_notify_err(window, cx);
471 } else {
472 cx.propagate();
473 }
474 });
475 register_action(editor, window, |editor, action, window, cx| {
476 if let Some(task) = editor.find_all_references(action, window, cx) {
477 task.detach_and_log_err(cx);
478 } else {
479 cx.propagate();
480 }
481 });
482 register_action(editor, window, Editor::show_signature_help);
483 register_action(editor, window, Editor::next_edit_prediction);
484 register_action(editor, window, Editor::previous_edit_prediction);
485 register_action(editor, window, Editor::show_inline_completion);
486 register_action(editor, window, Editor::context_menu_first);
487 register_action(editor, window, Editor::context_menu_prev);
488 register_action(editor, window, Editor::context_menu_next);
489 register_action(editor, window, Editor::context_menu_last);
490 register_action(editor, window, Editor::display_cursor_names);
491 register_action(editor, window, Editor::unique_lines_case_insensitive);
492 register_action(editor, window, Editor::unique_lines_case_sensitive);
493 register_action(editor, window, Editor::accept_partial_inline_completion);
494 register_action(editor, window, Editor::accept_edit_prediction);
495 register_action(editor, window, Editor::restore_file);
496 register_action(editor, window, Editor::git_restore);
497 register_action(editor, window, Editor::apply_all_diff_hunks);
498 register_action(editor, window, Editor::apply_selected_diff_hunks);
499 register_action(editor, window, Editor::open_active_item_in_terminal);
500 register_action(editor, window, Editor::reload_file);
501 register_action(editor, window, Editor::spawn_nearest_task);
502 register_action(editor, window, Editor::insert_uuid_v4);
503 register_action(editor, window, Editor::insert_uuid_v7);
504 register_action(editor, window, Editor::open_selections_in_multibuffer);
505 }
506
507 fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {
508 let position_map = layout.position_map.clone();
509 window.on_key_event({
510 let editor = self.editor.clone();
511 move |event: &ModifiersChangedEvent, phase, window, cx| {
512 if phase != DispatchPhase::Bubble {
513 return;
514 }
515 editor.update(cx, |editor, cx| {
516 if editor.hover_state.focused(window, cx) {
517 return;
518 }
519
520 editor.handle_modifiers_changed(event.modifiers, &position_map, window, cx);
521 })
522 }
523 });
524 }
525
526 fn mouse_left_down(
527 editor: &mut Editor,
528 event: &MouseDownEvent,
529 hovered_hunk: Option<Range<Anchor>>,
530 position_map: &PositionMap,
531 line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
532 window: &mut Window,
533 cx: &mut Context<Editor>,
534 ) {
535 if window.default_prevented() {
536 return;
537 }
538
539 let text_hitbox = &position_map.text_hitbox;
540 let gutter_hitbox = &position_map.gutter_hitbox;
541 let mut click_count = event.click_count;
542 let mut modifiers = event.modifiers;
543
544 if let Some(hovered_hunk) = hovered_hunk {
545 editor.toggle_diff_hunks_in_ranges_narrow(vec![hovered_hunk], cx);
546 cx.notify();
547 return;
548 } else if gutter_hitbox.is_hovered(window) {
549 click_count = 3; // Simulate triple-click when clicking the gutter to select lines
550 } else if !text_hitbox.is_hovered(window) {
551 return;
552 }
553
554 let is_singleton = editor.buffer().read(cx).is_singleton();
555
556 if click_count == 2 && !is_singleton {
557 match EditorSettings::get_global(cx).double_click_in_multibuffer {
558 DoubleClickInMultibuffer::Select => {
559 // do nothing special on double click, all selection logic is below
560 }
561 DoubleClickInMultibuffer::Open => {
562 if modifiers.alt {
563 // if double click is made with alt, pretend it's a regular double click without opening and alt,
564 // and run the selection logic.
565 modifiers.alt = false;
566 } else {
567 let scroll_position_row =
568 position_map.scroll_pixel_position.y / position_map.line_height;
569 let display_row = (((event.position - gutter_hitbox.bounds.origin).y
570 + position_map.scroll_pixel_position.y)
571 / position_map.line_height)
572 as u32;
573 let multi_buffer_row = position_map
574 .snapshot
575 .display_point_to_point(
576 DisplayPoint::new(DisplayRow(display_row), 0),
577 Bias::Right,
578 )
579 .row;
580 let line_offset_from_top = display_row - scroll_position_row as u32;
581 // if double click is made without alt, open the corresponding excerp
582 editor.open_excerpts_common(
583 Some(JumpData::MultiBufferRow {
584 row: MultiBufferRow(multi_buffer_row),
585 line_offset_from_top,
586 }),
587 false,
588 window,
589 cx,
590 );
591 return;
592 }
593 }
594 }
595 }
596
597 let point_for_position = position_map.point_for_position(event.position);
598 let position = point_for_position.previous_valid;
599 if modifiers == COLUMNAR_SELECTION_MODIFIERS {
600 editor.select(
601 SelectPhase::BeginColumnar {
602 position,
603 reset: false,
604 goal_column: point_for_position.exact_unclipped.column(),
605 },
606 window,
607 cx,
608 );
609 } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.secondary()
610 {
611 editor.select(
612 SelectPhase::Extend {
613 position,
614 click_count,
615 },
616 window,
617 cx,
618 );
619 } else {
620 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
621 let multi_cursor_modifier = match multi_cursor_setting {
622 MultiCursorModifier::Alt => modifiers.alt,
623 MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
624 };
625 editor.select(
626 SelectPhase::Begin {
627 position,
628 add: multi_cursor_modifier,
629 click_count,
630 },
631 window,
632 cx,
633 );
634 }
635 cx.stop_propagation();
636
637 if !is_singleton {
638 let display_row = (((event.position - gutter_hitbox.bounds.origin).y
639 + position_map.scroll_pixel_position.y)
640 / position_map.line_height) as u32;
641 let multi_buffer_row = position_map
642 .snapshot
643 .display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right)
644 .row;
645 if line_numbers
646 .get(&MultiBufferRow(multi_buffer_row))
647 .and_then(|line_number| line_number.hitbox.as_ref())
648 .is_some_and(|hitbox| hitbox.contains(&event.position))
649 {
650 let scroll_position_row =
651 position_map.scroll_pixel_position.y / position_map.line_height;
652 let line_offset_from_top = display_row - scroll_position_row as u32;
653
654 editor.open_excerpts_common(
655 Some(JumpData::MultiBufferRow {
656 row: MultiBufferRow(multi_buffer_row),
657 line_offset_from_top,
658 }),
659 modifiers.alt,
660 window,
661 cx,
662 );
663 cx.stop_propagation();
664 }
665 }
666 }
667
668 fn mouse_right_down(
669 editor: &mut Editor,
670 event: &MouseDownEvent,
671 position_map: &PositionMap,
672 window: &mut Window,
673 cx: &mut Context<Editor>,
674 ) {
675 if !position_map.text_hitbox.is_hovered(window) {
676 return;
677 }
678 let point_for_position = position_map.point_for_position(event.position);
679 mouse_context_menu::deploy_context_menu(
680 editor,
681 Some(event.position),
682 point_for_position.previous_valid,
683 window,
684 cx,
685 );
686 cx.stop_propagation();
687 }
688
689 fn mouse_middle_down(
690 editor: &mut Editor,
691 event: &MouseDownEvent,
692 position_map: &PositionMap,
693 window: &mut Window,
694 cx: &mut Context<Editor>,
695 ) {
696 if !position_map.text_hitbox.is_hovered(window) || window.default_prevented() {
697 return;
698 }
699
700 let point_for_position = position_map.point_for_position(event.position);
701 let position = point_for_position.previous_valid;
702
703 editor.select(
704 SelectPhase::BeginColumnar {
705 position,
706 reset: true,
707 goal_column: point_for_position.exact_unclipped.column(),
708 },
709 window,
710 cx,
711 );
712 }
713
714 fn mouse_up(
715 editor: &mut Editor,
716 event: &MouseUpEvent,
717 position_map: &PositionMap,
718 window: &mut Window,
719 cx: &mut Context<Editor>,
720 ) {
721 let text_hitbox = &position_map.text_hitbox;
722 let end_selection = editor.has_pending_selection();
723 let pending_nonempty_selections = editor.has_pending_nonempty_selection();
724
725 if end_selection {
726 editor.select(SelectPhase::End, window, cx);
727 }
728
729 if end_selection && pending_nonempty_selections {
730 cx.stop_propagation();
731 } else if cfg!(any(target_os = "linux", target_os = "freebsd"))
732 && event.button == MouseButton::Middle
733 {
734 if !text_hitbox.is_hovered(window) || editor.read_only(cx) {
735 return;
736 }
737
738 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
739 if EditorSettings::get_global(cx).middle_click_paste {
740 if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
741 let point_for_position = position_map.point_for_position(event.position);
742 let position = point_for_position.previous_valid;
743
744 editor.select(
745 SelectPhase::Begin {
746 position,
747 add: false,
748 click_count: 1,
749 },
750 window,
751 cx,
752 );
753 editor.insert(&text, window, cx);
754 }
755 cx.stop_propagation()
756 }
757 }
758 }
759
760 fn click(
761 editor: &mut Editor,
762 event: &ClickEvent,
763 position_map: &PositionMap,
764 window: &mut Window,
765 cx: &mut Context<Editor>,
766 ) {
767 let text_hitbox = &position_map.text_hitbox;
768 let pending_nonempty_selections = editor.has_pending_nonempty_selection();
769
770 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
771 let multi_cursor_modifier = match multi_cursor_setting {
772 MultiCursorModifier::Alt => event.modifiers().secondary(),
773 MultiCursorModifier::CmdOrCtrl => event.modifiers().alt,
774 };
775
776 if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(window) {
777 let point = position_map.point_for_position(event.up.position);
778 editor.handle_click_hovered_link(point, event.modifiers(), window, cx);
779
780 cx.stop_propagation();
781 }
782 }
783
784 fn mouse_dragged(
785 editor: &mut Editor,
786 event: &MouseMoveEvent,
787 position_map: &PositionMap,
788 window: &mut Window,
789 cx: &mut Context<Editor>,
790 ) {
791 if !editor.has_pending_selection() {
792 return;
793 }
794
795 let text_bounds = position_map.text_hitbox.bounds;
796 let point_for_position = position_map.point_for_position(event.position);
797 let mut scroll_delta = gpui::Point::<f32>::default();
798 let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
799 let top = text_bounds.origin.y + vertical_margin;
800 let bottom = text_bounds.bottom_left().y - vertical_margin;
801 if event.position.y < top {
802 scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);
803 }
804 if event.position.y > bottom {
805 scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);
806 }
807
808 // We need horizontal width of text
809 let style = editor.style.clone().unwrap_or_default();
810 let font_id = window.text_system().resolve_font(&style.text.font());
811 let font_size = style.text.font_size.to_pixels(window.rem_size());
812 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
813
814 let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin;
815
816 let scroll_space: Pixels = scroll_margin_x * em_width;
817
818 let left = text_bounds.origin.x + scroll_space;
819 let right = text_bounds.top_right().x - scroll_space;
820
821 if event.position.x < left {
822 scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
823 }
824 if event.position.x > right {
825 scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
826 }
827
828 editor.select(
829 SelectPhase::Update {
830 position: point_for_position.previous_valid,
831 goal_column: point_for_position.exact_unclipped.column(),
832 scroll_delta,
833 },
834 window,
835 cx,
836 );
837 }
838
839 fn mouse_moved(
840 editor: &mut Editor,
841 event: &MouseMoveEvent,
842 position_map: &PositionMap,
843 window: &mut Window,
844 cx: &mut Context<Editor>,
845 ) {
846 let text_hitbox = &position_map.text_hitbox;
847 let gutter_hitbox = &position_map.gutter_hitbox;
848 let modifiers = event.modifiers;
849 let gutter_hovered = gutter_hitbox.is_hovered(window);
850 editor.set_gutter_hovered(gutter_hovered, cx);
851
852 // Don't trigger hover popover if mouse is hovering over context menu
853 if text_hitbox.is_hovered(window) {
854 let point_for_position = position_map.point_for_position(event.position);
855
856 editor.update_hovered_link(
857 point_for_position,
858 &position_map.snapshot,
859 modifiers,
860 window,
861 cx,
862 );
863
864 if let Some(point) = point_for_position.as_valid() {
865 let anchor = position_map
866 .snapshot
867 .buffer_snapshot
868 .anchor_before(point.to_offset(&position_map.snapshot, Bias::Left));
869 hover_at(editor, Some(anchor), window, cx);
870 Self::update_visible_cursor(editor, point, position_map, window, cx);
871 } else {
872 hover_at(editor, None, window, cx);
873 }
874 } else {
875 editor.hide_hovered_link(cx);
876 hover_at(editor, None, window, cx);
877 if gutter_hovered {
878 cx.stop_propagation();
879 }
880 }
881 }
882
883 fn update_visible_cursor(
884 editor: &mut Editor,
885 point: DisplayPoint,
886 position_map: &PositionMap,
887 window: &mut Window,
888 cx: &mut Context<Editor>,
889 ) {
890 let snapshot = &position_map.snapshot;
891 let Some(hub) = editor.collaboration_hub() else {
892 return;
893 };
894 let start = snapshot.display_snapshot.clip_point(
895 DisplayPoint::new(point.row(), point.column().saturating_sub(1)),
896 Bias::Left,
897 );
898 let end = snapshot.display_snapshot.clip_point(
899 DisplayPoint::new(
900 point.row(),
901 (point.column() + 1).min(snapshot.line_len(point.row())),
902 ),
903 Bias::Right,
904 );
905
906 let range = snapshot
907 .buffer_snapshot
908 .anchor_at(start.to_point(&snapshot.display_snapshot), Bias::Left)
909 ..snapshot
910 .buffer_snapshot
911 .anchor_at(end.to_point(&snapshot.display_snapshot), Bias::Right);
912
913 let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
914 return;
915 };
916 let key = crate::HoveredCursor {
917 replica_id: selection.replica_id,
918 selection_id: selection.selection.id,
919 };
920 editor.hovered_cursors.insert(
921 key.clone(),
922 cx.spawn_in(window, |editor, mut cx| async move {
923 cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
924 editor
925 .update(&mut cx, |editor, cx| {
926 editor.hovered_cursors.remove(&key);
927 cx.notify();
928 })
929 .ok();
930 }),
931 );
932 cx.notify()
933 }
934
935 #[allow(clippy::too_many_arguments)]
936 fn layout_selections(
937 &self,
938 start_anchor: Anchor,
939 end_anchor: Anchor,
940 local_selections: &[Selection<Point>],
941 snapshot: &EditorSnapshot,
942 start_row: DisplayRow,
943 end_row: DisplayRow,
944 window: &mut Window,
945 cx: &mut App,
946 ) -> (
947 Vec<(PlayerColor, Vec<SelectionLayout>)>,
948 BTreeMap<DisplayRow, bool>,
949 Option<DisplayPoint>,
950 ) {
951 let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
952 let mut active_rows = BTreeMap::new();
953 let mut newest_selection_head = None;
954 self.editor.update(cx, |editor, cx| {
955 if editor.show_local_selections {
956 let mut layouts = Vec::new();
957 let newest = editor.selections.newest(cx);
958 for selection in local_selections.iter().cloned() {
959 let is_empty = selection.start == selection.end;
960 let is_newest = selection == newest;
961
962 let layout = SelectionLayout::new(
963 selection,
964 editor.selections.line_mode,
965 editor.cursor_shape,
966 &snapshot.display_snapshot,
967 is_newest,
968 editor.leader_peer_id.is_none(),
969 None,
970 );
971 if is_newest {
972 newest_selection_head = Some(layout.head);
973 }
974
975 for row in cmp::max(layout.active_rows.start.0, start_row.0)
976 ..=cmp::min(layout.active_rows.end.0, end_row.0)
977 {
978 let contains_non_empty_selection =
979 active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
980 *contains_non_empty_selection |= !is_empty;
981 }
982 layouts.push(layout);
983 }
984
985 let player = editor.current_user_player_color(cx);
986 selections.push((player, layouts));
987 }
988
989 if let Some(collaboration_hub) = &editor.collaboration_hub {
990 // When following someone, render the local selections in their color.
991 if let Some(leader_id) = editor.leader_peer_id {
992 if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id)
993 {
994 if let Some(participant_index) = collaboration_hub
995 .user_participant_indices(cx)
996 .get(&collaborator.user_id)
997 {
998 if let Some((local_selection_style, _)) = selections.first_mut() {
999 *local_selection_style = cx
1000 .theme()
1001 .players()
1002 .color_for_participant(participant_index.0);
1003 }
1004 }
1005 }
1006 }
1007
1008 let mut remote_selections = HashMap::default();
1009 for selection in snapshot.remote_selections_in_range(
1010 &(start_anchor..end_anchor),
1011 collaboration_hub.as_ref(),
1012 cx,
1013 ) {
1014 let selection_style =
1015 Self::get_participant_color(selection.participant_index, cx);
1016
1017 // Don't re-render the leader's selections, since the local selections
1018 // match theirs.
1019 if Some(selection.peer_id) == editor.leader_peer_id {
1020 continue;
1021 }
1022 let key = HoveredCursor {
1023 replica_id: selection.replica_id,
1024 selection_id: selection.selection.id,
1025 };
1026
1027 let is_shown =
1028 editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
1029
1030 remote_selections
1031 .entry(selection.replica_id)
1032 .or_insert((selection_style, Vec::new()))
1033 .1
1034 .push(SelectionLayout::new(
1035 selection.selection,
1036 selection.line_mode,
1037 selection.cursor_shape,
1038 &snapshot.display_snapshot,
1039 false,
1040 false,
1041 if is_shown { selection.user_name } else { None },
1042 ));
1043 }
1044
1045 selections.extend(remote_selections.into_values());
1046 } else if !editor.is_focused(window) && editor.show_cursor_when_unfocused {
1047 let layouts = snapshot
1048 .buffer_snapshot
1049 .selections_in_range(&(start_anchor..end_anchor), true)
1050 .map(move |(_, line_mode, cursor_shape, selection)| {
1051 SelectionLayout::new(
1052 selection,
1053 line_mode,
1054 cursor_shape,
1055 &snapshot.display_snapshot,
1056 false,
1057 false,
1058 None,
1059 )
1060 })
1061 .collect::<Vec<_>>();
1062 let player = editor.current_user_player_color(cx);
1063 selections.push((player, layouts));
1064 }
1065 });
1066 (selections, active_rows, newest_selection_head)
1067 }
1068
1069 fn collect_cursors(
1070 &self,
1071 snapshot: &EditorSnapshot,
1072 cx: &mut App,
1073 ) -> Vec<(DisplayPoint, Hsla)> {
1074 let editor = self.editor.read(cx);
1075 let mut cursors = Vec::new();
1076 let mut skip_local = false;
1077 let mut add_cursor = |anchor: Anchor, color| {
1078 cursors.push((anchor.to_display_point(&snapshot.display_snapshot), color));
1079 };
1080 // Remote cursors
1081 if let Some(collaboration_hub) = &editor.collaboration_hub {
1082 for remote_selection in snapshot.remote_selections_in_range(
1083 &(Anchor::min()..Anchor::max()),
1084 collaboration_hub.deref(),
1085 cx,
1086 ) {
1087 let color = Self::get_participant_color(remote_selection.participant_index, cx);
1088 add_cursor(remote_selection.selection.head(), color.cursor);
1089 if Some(remote_selection.peer_id) == editor.leader_peer_id {
1090 skip_local = true;
1091 }
1092 }
1093 }
1094 // Local cursors
1095 if !skip_local {
1096 let color = cx.theme().players().local().cursor;
1097 editor.selections.disjoint.iter().for_each(|selection| {
1098 add_cursor(selection.head(), color);
1099 });
1100 if let Some(ref selection) = editor.selections.pending_anchor() {
1101 add_cursor(selection.head(), color);
1102 }
1103 }
1104 cursors
1105 }
1106
1107 #[allow(clippy::too_many_arguments)]
1108 fn layout_visible_cursors(
1109 &self,
1110 snapshot: &EditorSnapshot,
1111 selections: &[(PlayerColor, Vec<SelectionLayout>)],
1112 block_start_rows: &HashSet<DisplayRow>,
1113 visible_display_row_range: Range<DisplayRow>,
1114 line_layouts: &[LineWithInvisibles],
1115 text_hitbox: &Hitbox,
1116 content_origin: gpui::Point<Pixels>,
1117 scroll_position: gpui::Point<f32>,
1118 scroll_pixel_position: gpui::Point<Pixels>,
1119 line_height: Pixels,
1120 em_width: Pixels,
1121 em_advance: Pixels,
1122 autoscroll_containing_element: bool,
1123 window: &mut Window,
1124 cx: &mut App,
1125 ) -> Vec<CursorLayout> {
1126 let mut autoscroll_bounds = None;
1127 let cursor_layouts = self.editor.update(cx, |editor, cx| {
1128 let mut cursors = Vec::new();
1129
1130 let show_local_cursors = editor.show_local_cursors(window, cx);
1131
1132 for (player_color, selections) in selections {
1133 for selection in selections {
1134 let cursor_position = selection.head;
1135
1136 let in_range = visible_display_row_range.contains(&cursor_position.row());
1137 if (selection.is_local && !show_local_cursors)
1138 || !in_range
1139 || block_start_rows.contains(&cursor_position.row())
1140 {
1141 continue;
1142 }
1143
1144 let cursor_row_layout = &line_layouts
1145 [cursor_position.row().minus(visible_display_row_range.start) as usize];
1146 let cursor_column = cursor_position.column() as usize;
1147
1148 let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
1149 let mut block_width =
1150 cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
1151 if block_width == Pixels::ZERO {
1152 block_width = em_advance;
1153 }
1154 let block_text = if let CursorShape::Block = selection.cursor_shape {
1155 snapshot
1156 .grapheme_at(cursor_position)
1157 .or_else(|| {
1158 if cursor_column == 0 {
1159 snapshot.placeholder_text().and_then(|s| {
1160 s.graphemes(true).next().map(|s| s.to_string().into())
1161 })
1162 } else {
1163 None
1164 }
1165 })
1166 .and_then(|text| {
1167 let len = text.len();
1168
1169 let font = cursor_row_layout
1170 .font_id_for_index(cursor_column)
1171 .and_then(|cursor_font_id| {
1172 window.text_system().get_font_for_id(cursor_font_id)
1173 })
1174 .unwrap_or(self.style.text.font());
1175
1176 // Invert the text color for the block cursor. Ensure that the text
1177 // color is opaque enough to be visible against the background color.
1178 //
1179 // 0.75 is an arbitrary threshold to determine if the background color is
1180 // opaque enough to use as a text color.
1181 //
1182 // TODO: In the future we should ensure themes have a `text_inverse` color.
1183 let color = if cx.theme().colors().editor_background.a < 0.75 {
1184 match cx.theme().appearance {
1185 Appearance::Dark => Hsla::black(),
1186 Appearance::Light => Hsla::white(),
1187 }
1188 } else {
1189 cx.theme().colors().editor_background
1190 };
1191
1192 window
1193 .text_system()
1194 .shape_line(
1195 text,
1196 cursor_row_layout.font_size,
1197 &[TextRun {
1198 len,
1199 font,
1200 color,
1201 background_color: None,
1202 strikethrough: None,
1203 underline: None,
1204 }],
1205 )
1206 .log_err()
1207 })
1208 } else {
1209 None
1210 };
1211
1212 let x = cursor_character_x - scroll_pixel_position.x;
1213 let y = (cursor_position.row().as_f32()
1214 - scroll_pixel_position.y / line_height)
1215 * line_height;
1216 if selection.is_newest {
1217 editor.pixel_position_of_newest_cursor = Some(point(
1218 text_hitbox.origin.x + x + block_width / 2.,
1219 text_hitbox.origin.y + y + line_height / 2.,
1220 ));
1221
1222 if autoscroll_containing_element {
1223 let top = text_hitbox.origin.y
1224 + (cursor_position.row().as_f32() - scroll_position.y - 3.).max(0.)
1225 * line_height;
1226 let left = text_hitbox.origin.x
1227 + (cursor_position.column() as f32 - scroll_position.x - 3.)
1228 .max(0.)
1229 * em_width;
1230
1231 let bottom = text_hitbox.origin.y
1232 + (cursor_position.row().as_f32() - scroll_position.y + 4.)
1233 * line_height;
1234 let right = text_hitbox.origin.x
1235 + (cursor_position.column() as f32 - scroll_position.x + 4.)
1236 * em_width;
1237
1238 autoscroll_bounds =
1239 Some(Bounds::from_corners(point(left, top), point(right, bottom)))
1240 }
1241 }
1242
1243 let mut cursor = CursorLayout {
1244 color: player_color.cursor,
1245 block_width,
1246 origin: point(x, y),
1247 line_height,
1248 shape: selection.cursor_shape,
1249 block_text,
1250 cursor_name: None,
1251 };
1252 let cursor_name = selection.user_name.clone().map(|name| CursorName {
1253 string: name,
1254 color: self.style.background,
1255 is_top_row: cursor_position.row().0 == 0,
1256 });
1257 cursor.layout(content_origin, cursor_name, window, cx);
1258 cursors.push(cursor);
1259 }
1260 }
1261
1262 cursors
1263 });
1264
1265 if let Some(bounds) = autoscroll_bounds {
1266 window.request_autoscroll(bounds);
1267 }
1268
1269 cursor_layouts
1270 }
1271
1272 fn layout_scrollbars(
1273 &self,
1274 snapshot: &EditorSnapshot,
1275 scrollbar_range_data: ScrollbarRangeData,
1276 scroll_position: gpui::Point<f32>,
1277 non_visible_cursors: bool,
1278 window: &mut Window,
1279 cx: &mut App,
1280 ) -> AxisPair<Option<ScrollbarLayout>> {
1281 let letter_size = scrollbar_range_data.letter_size;
1282 let text_units_per_page = axis_pair(
1283 scrollbar_range_data.scrollbar_bounds.size.width / letter_size.width,
1284 scrollbar_range_data.scrollbar_bounds.size.height / letter_size.height,
1285 );
1286
1287 let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
1288 let show_scrollbars = self.editor.read(cx).show_scrollbars
1289 && match scrollbar_settings.show {
1290 ShowScrollbar::Auto => {
1291 let editor = self.editor.read(cx);
1292 let is_singleton = editor.is_singleton(cx);
1293 // Git
1294 (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_diff_hunks())
1295 ||
1296 // Buffer Search Results
1297 (is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
1298 ||
1299 // Selected Text Occurrences
1300 (is_singleton && scrollbar_settings.selected_text && editor.has_background_highlights::<SelectedTextHighlight>())
1301 ||
1302 // Selected Symbol Occurrences
1303 (is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
1304 ||
1305 // Diagnostics
1306 (is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot.has_diagnostics())
1307 ||
1308 // Cursors out of sight
1309 non_visible_cursors
1310 ||
1311 // Scrollmanager
1312 editor.scroll_manager.scrollbars_visible()
1313 }
1314 ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
1315 ShowScrollbar::Always => true,
1316 ShowScrollbar::Never => false,
1317 };
1318
1319 let axes: AxisPair<bool> = scrollbar_settings.axes.into();
1320
1321 if snapshot.mode != EditorMode::Full {
1322 return axis_pair(None, None);
1323 }
1324
1325 let visible_range = axis_pair(
1326 axes.horizontal
1327 .then(|| scroll_position.x..scroll_position.x + text_units_per_page.horizontal),
1328 axes.vertical
1329 .then(|| scroll_position.y..scroll_position.y + text_units_per_page.vertical),
1330 );
1331
1332 // If a drag took place after we started dragging the scrollbar,
1333 // cancel the scrollbar drag.
1334 if cx.has_active_drag() {
1335 self.editor.update(cx, |editor, cx| {
1336 editor
1337 .scroll_manager
1338 .set_is_dragging_scrollbar(Axis::Horizontal, false, cx);
1339 editor
1340 .scroll_manager
1341 .set_is_dragging_scrollbar(Axis::Vertical, false, cx);
1342 });
1343 }
1344
1345 let text_bounds = scrollbar_range_data.scrollbar_bounds;
1346
1347 let track_bounds = axis_pair(
1348 axes.horizontal.then(|| {
1349 Bounds::from_corners(
1350 point(
1351 text_bounds.bottom_left().x,
1352 text_bounds.bottom_left().y - self.style.scrollbar_width,
1353 ),
1354 point(
1355 text_bounds.bottom_right().x
1356 - if axes.vertical {
1357 self.style.scrollbar_width
1358 } else {
1359 px(0.)
1360 },
1361 text_bounds.bottom_right().y,
1362 ),
1363 )
1364 }),
1365 axes.vertical.then(|| {
1366 Bounds::from_corners(
1367 point(self.scrollbar_left(&text_bounds), text_bounds.origin.y),
1368 text_bounds.bottom_right(),
1369 )
1370 }),
1371 );
1372
1373 let scroll_range_size = scrollbar_range_data.scroll_range.size;
1374 let total_text_units = axis_pair(
1375 Some(scroll_range_size.width / letter_size.width),
1376 Some(scroll_range_size.height / letter_size.height),
1377 );
1378
1379 let thumb_size = axis_pair(
1380 total_text_units
1381 .horizontal
1382 .zip(track_bounds.horizontal)
1383 .and_then(|(total_text_units_x, track_bounds_x)| {
1384 if text_units_per_page.horizontal >= total_text_units_x {
1385 return None;
1386 }
1387 if track_bounds_x.size.width < px(MIN_SCROLL_THUMB_SIZE) {
1388 return Some(track_bounds_x.size.width);
1389 }
1390 let thumb_size = track_bounds_x.size.width
1391 * (text_units_per_page.horizontal / total_text_units_x);
1392 Some(thumb_size.clamp(px(MIN_SCROLL_THUMB_SIZE), track_bounds_x.size.width))
1393 }),
1394 total_text_units.vertical.zip(track_bounds.vertical).map(
1395 |(total_text_units_y, track_bounds_y)| {
1396 if track_bounds_y.size.height < px(MIN_SCROLL_THUMB_SIZE) {
1397 return track_bounds_y.size.height;
1398 }
1399 let thumb_size = track_bounds_y.size.height
1400 * (text_units_per_page.vertical / total_text_units_y);
1401 thumb_size.clamp(px(MIN_SCROLL_THUMB_SIZE), track_bounds_y.size.height)
1402 },
1403 ),
1404 );
1405
1406 // NOTE: Space not taken by track bounds divided by text units not on screen
1407 let text_unit_size = axis_pair(
1408 thumb_size
1409 .horizontal
1410 .zip(track_bounds.horizontal)
1411 .zip(total_text_units.horizontal)
1412 .map(|((thumb_size, track_bounds), total_text_units)| {
1413 (track_bounds.size.width - thumb_size)
1414 / (total_text_units - text_units_per_page.horizontal).max(0.)
1415 }),
1416 thumb_size
1417 .vertical
1418 .zip(track_bounds.vertical)
1419 .zip(total_text_units.vertical)
1420 .map(|((thumb_size, track_bounds), total_text_units)| {
1421 (track_bounds.size.height - thumb_size)
1422 / (total_text_units - text_units_per_page.vertical).max(0.)
1423 }),
1424 );
1425
1426 let horizontal_scrollbar = track_bounds
1427 .horizontal
1428 .zip(visible_range.horizontal)
1429 .zip(text_unit_size.horizontal)
1430 .zip(thumb_size.horizontal)
1431 .map(
1432 |(((track_bounds, visible_range), text_unit_size), thumb_size)| ScrollbarLayout {
1433 hitbox: window.insert_hitbox(track_bounds, false),
1434 visible_range,
1435 text_unit_size,
1436 visible: show_scrollbars,
1437 thumb_size,
1438 axis: Axis::Horizontal,
1439 },
1440 );
1441
1442 let vertical_scrollbar = track_bounds
1443 .vertical
1444 .zip(visible_range.vertical)
1445 .zip(text_unit_size.vertical)
1446 .zip(thumb_size.vertical)
1447 .map(
1448 |(((track_bounds, visible_range), text_unit_size), thumb_size)| ScrollbarLayout {
1449 hitbox: window.insert_hitbox(track_bounds, false),
1450 visible_range,
1451 text_unit_size,
1452 visible: show_scrollbars,
1453 thumb_size,
1454 axis: Axis::Vertical,
1455 },
1456 );
1457
1458 axis_pair(horizontal_scrollbar, vertical_scrollbar)
1459 }
1460
1461 #[allow(clippy::too_many_arguments)]
1462 fn prepaint_crease_toggles(
1463 &self,
1464 crease_toggles: &mut [Option<AnyElement>],
1465 line_height: Pixels,
1466 gutter_dimensions: &GutterDimensions,
1467 gutter_settings: crate::editor_settings::Gutter,
1468 scroll_pixel_position: gpui::Point<Pixels>,
1469 gutter_hitbox: &Hitbox,
1470 window: &mut Window,
1471 cx: &mut App,
1472 ) {
1473 for (ix, crease_toggle) in crease_toggles.iter_mut().enumerate() {
1474 if let Some(crease_toggle) = crease_toggle {
1475 debug_assert!(gutter_settings.folds);
1476 let available_space = size(
1477 AvailableSpace::MinContent,
1478 AvailableSpace::Definite(line_height * 0.55),
1479 );
1480 let crease_toggle_size = crease_toggle.layout_as_root(available_space, window, cx);
1481
1482 let position = point(
1483 gutter_dimensions.width - gutter_dimensions.right_padding,
1484 ix as f32 * line_height - (scroll_pixel_position.y % line_height),
1485 );
1486 let centering_offset = point(
1487 (gutter_dimensions.fold_area_width() - crease_toggle_size.width) / 2.,
1488 (line_height - crease_toggle_size.height) / 2.,
1489 );
1490 let origin = gutter_hitbox.origin + position + centering_offset;
1491 crease_toggle.prepaint_as_root(origin, available_space, window, cx);
1492 }
1493 }
1494 }
1495
1496 #[allow(clippy::too_many_arguments)]
1497 fn prepaint_crease_trailers(
1498 &self,
1499 trailers: Vec<Option<AnyElement>>,
1500 lines: &[LineWithInvisibles],
1501 line_height: Pixels,
1502 content_origin: gpui::Point<Pixels>,
1503 scroll_pixel_position: gpui::Point<Pixels>,
1504 em_width: Pixels,
1505 window: &mut Window,
1506 cx: &mut App,
1507 ) -> Vec<Option<CreaseTrailerLayout>> {
1508 trailers
1509 .into_iter()
1510 .enumerate()
1511 .map(|(ix, element)| {
1512 let mut element = element?;
1513 let available_space = size(
1514 AvailableSpace::MinContent,
1515 AvailableSpace::Definite(line_height),
1516 );
1517 let size = element.layout_as_root(available_space, window, cx);
1518
1519 let line = &lines[ix];
1520 let padding = if line.width == Pixels::ZERO {
1521 Pixels::ZERO
1522 } else {
1523 4. * em_width
1524 };
1525 let position = point(
1526 scroll_pixel_position.x + line.width + padding,
1527 ix as f32 * line_height - (scroll_pixel_position.y % line_height),
1528 );
1529 let centering_offset = point(px(0.), (line_height - size.height) / 2.);
1530 let origin = content_origin + position + centering_offset;
1531 element.prepaint_as_root(origin, available_space, window, cx);
1532 Some(CreaseTrailerLayout {
1533 element,
1534 bounds: Bounds::new(origin, size),
1535 })
1536 })
1537 .collect()
1538 }
1539
1540 // Folds contained in a hunk are ignored apart from shrinking visual size
1541 // If a fold contains any hunks then that fold line is marked as modified
1542 fn layout_gutter_diff_hunks(
1543 &self,
1544 line_height: Pixels,
1545 gutter_hitbox: &Hitbox,
1546 display_rows: Range<DisplayRow>,
1547 snapshot: &EditorSnapshot,
1548 window: &mut Window,
1549 cx: &mut App,
1550 ) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
1551 let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(snapshot);
1552 let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(snapshot);
1553
1554 let mut display_hunks = Vec::<(DisplayDiffHunk, Option<Hitbox>)>::new();
1555 let folded_buffers = self.editor.read(cx).folded_buffers(cx);
1556
1557 for hunk in snapshot
1558 .buffer_snapshot
1559 .diff_hunks_in_range(buffer_start..buffer_end)
1560 {
1561 if folded_buffers.contains(&hunk.buffer_id) {
1562 continue;
1563 }
1564
1565 let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
1566 let hunk_end_point = Point::new(hunk.row_range.end.0, 0);
1567
1568 let hunk_display_start = snapshot.point_to_display_point(hunk_start_point, Bias::Left);
1569 let hunk_display_end = snapshot.point_to_display_point(hunk_end_point, Bias::Right);
1570
1571 let display_hunk = if hunk_display_start.column() != 0 {
1572 DisplayDiffHunk::Folded {
1573 display_row: hunk_display_start.row(),
1574 }
1575 } else {
1576 let mut end_row = hunk_display_end.row();
1577 if hunk_display_end.column() > 0 {
1578 end_row.0 += 1;
1579 }
1580 DisplayDiffHunk::Unfolded {
1581 status: hunk.status(),
1582 diff_base_byte_range: hunk.diff_base_byte_range,
1583 display_row_range: hunk_display_start.row()..end_row,
1584 multi_buffer_range: Anchor::range_in_buffer(
1585 hunk.excerpt_id,
1586 hunk.buffer_id,
1587 hunk.buffer_range,
1588 ),
1589 }
1590 };
1591
1592 display_hunks.push((display_hunk, None));
1593 }
1594
1595 let git_gutter_setting = ProjectSettings::get_global(cx)
1596 .git
1597 .git_gutter
1598 .unwrap_or_default();
1599 if let GitGutterSetting::TrackedFiles = git_gutter_setting {
1600 for (hunk, hitbox) in &mut display_hunks {
1601 if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
1602 let hunk_bounds =
1603 Self::diff_hunk_bounds(snapshot, line_height, gutter_hitbox.bounds, hunk);
1604 *hitbox = Some(window.insert_hitbox(hunk_bounds, true));
1605 }
1606 }
1607 }
1608
1609 display_hunks
1610 }
1611
1612 #[allow(clippy::too_many_arguments)]
1613 fn layout_inline_blame(
1614 &self,
1615 display_row: DisplayRow,
1616 row_info: &RowInfo,
1617 line_layout: &LineWithInvisibles,
1618 crease_trailer: Option<&CreaseTrailerLayout>,
1619 em_width: Pixels,
1620 content_origin: gpui::Point<Pixels>,
1621 scroll_pixel_position: gpui::Point<Pixels>,
1622 line_height: Pixels,
1623 window: &mut Window,
1624 cx: &mut App,
1625 ) -> Option<AnyElement> {
1626 if !self
1627 .editor
1628 .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx))
1629 {
1630 return None;
1631 }
1632
1633 let editor = self.editor.read(cx);
1634 let blame = editor.blame.clone()?;
1635 let padding = {
1636 const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
1637 const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.;
1638
1639 let mut padding = INLINE_BLAME_PADDING_EM_WIDTHS;
1640
1641 if let Some(inline_completion) = editor.active_inline_completion.as_ref() {
1642 match &inline_completion.completion {
1643 InlineCompletion::Edit {
1644 display_mode: EditDisplayMode::TabAccept,
1645 ..
1646 } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS,
1647 _ => {}
1648 }
1649 }
1650
1651 padding * em_width
1652 };
1653
1654 let blame_entry = blame
1655 .update(cx, |blame, cx| {
1656 blame.blame_for_rows(&[*row_info], cx).next()
1657 })
1658 .flatten()?;
1659
1660 let mut element =
1661 render_inline_blame_entry(self.editor.clone(), &blame, blame_entry, &self.style, cx);
1662
1663 let start_y = content_origin.y
1664 + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
1665
1666 let start_x = {
1667 let line_end = if let Some(crease_trailer) = crease_trailer {
1668 crease_trailer.bounds.right()
1669 } else {
1670 content_origin.x - scroll_pixel_position.x + line_layout.width
1671 };
1672
1673 let padded_line_end = line_end + padding;
1674
1675 let min_column_in_pixels = ProjectSettings::get_global(cx)
1676 .git
1677 .inline_blame
1678 .and_then(|settings| settings.min_column)
1679 .map(|col| self.column_pixels(col as usize, window, cx))
1680 .unwrap_or(px(0.));
1681 let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
1682
1683 cmp::max(padded_line_end, min_start)
1684 };
1685
1686 let absolute_offset = point(start_x, start_y);
1687 element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
1688
1689 Some(element)
1690 }
1691
1692 #[allow(clippy::too_many_arguments)]
1693 fn layout_blame_entries(
1694 &self,
1695 buffer_rows: &[RowInfo],
1696 em_width: Pixels,
1697 scroll_position: gpui::Point<f32>,
1698 line_height: Pixels,
1699 gutter_hitbox: &Hitbox,
1700 max_width: Option<Pixels>,
1701 window: &mut Window,
1702 cx: &mut App,
1703 ) -> Option<Vec<AnyElement>> {
1704 if !self
1705 .editor
1706 .update(cx, |editor, cx| editor.render_git_blame_gutter(cx))
1707 {
1708 return None;
1709 }
1710
1711 let blame = self.editor.read(cx).blame.clone()?;
1712 let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
1713 blame.blame_for_rows(buffer_rows, cx).collect()
1714 });
1715
1716 let width = if let Some(max_width) = max_width {
1717 AvailableSpace::Definite(max_width)
1718 } else {
1719 AvailableSpace::MaxContent
1720 };
1721 let scroll_top = scroll_position.y * line_height;
1722 let start_x = em_width;
1723
1724 let mut last_used_color: Option<(PlayerColor, Oid)> = None;
1725
1726 let shaped_lines = blamed_rows
1727 .into_iter()
1728 .enumerate()
1729 .flat_map(|(ix, blame_entry)| {
1730 if let Some(blame_entry) = blame_entry {
1731 let mut element = render_blame_entry(
1732 ix,
1733 &blame,
1734 blame_entry,
1735 &self.style,
1736 &mut last_used_color,
1737 self.editor.clone(),
1738 cx,
1739 );
1740
1741 let start_y = ix as f32 * line_height - (scroll_top % line_height);
1742 let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
1743
1744 element.prepaint_as_root(
1745 absolute_offset,
1746 size(width, AvailableSpace::MinContent),
1747 window,
1748 cx,
1749 );
1750
1751 Some(element)
1752 } else {
1753 None
1754 }
1755 })
1756 .collect();
1757
1758 Some(shaped_lines)
1759 }
1760
1761 #[allow(clippy::too_many_arguments)]
1762 fn layout_indent_guides(
1763 &self,
1764 content_origin: gpui::Point<Pixels>,
1765 text_origin: gpui::Point<Pixels>,
1766 visible_buffer_range: Range<MultiBufferRow>,
1767 scroll_pixel_position: gpui::Point<Pixels>,
1768 line_height: Pixels,
1769 snapshot: &DisplaySnapshot,
1770 window: &mut Window,
1771 cx: &mut App,
1772 ) -> Option<Vec<IndentGuideLayout>> {
1773 let indent_guides = self.editor.update(cx, |editor, cx| {
1774 editor.indent_guides(visible_buffer_range, snapshot, cx)
1775 })?;
1776
1777 let active_indent_guide_indices = self.editor.update(cx, |editor, cx| {
1778 editor
1779 .find_active_indent_guide_indices(&indent_guides, snapshot, window, cx)
1780 .unwrap_or_default()
1781 });
1782
1783 Some(
1784 indent_guides
1785 .into_iter()
1786 .enumerate()
1787 .filter_map(|(i, indent_guide)| {
1788 let single_indent_width =
1789 self.column_pixels(indent_guide.tab_size as usize, window, cx);
1790 let total_width = single_indent_width * indent_guide.depth as f32;
1791 let start_x = content_origin.x + total_width - scroll_pixel_position.x;
1792 if start_x >= text_origin.x {
1793 let (offset_y, length) = Self::calculate_indent_guide_bounds(
1794 indent_guide.start_row..indent_guide.end_row,
1795 line_height,
1796 snapshot,
1797 );
1798
1799 let start_y = content_origin.y + offset_y - scroll_pixel_position.y;
1800
1801 Some(IndentGuideLayout {
1802 origin: point(start_x, start_y),
1803 length,
1804 single_indent_width,
1805 depth: indent_guide.depth,
1806 active: active_indent_guide_indices.contains(&i),
1807 settings: indent_guide.settings,
1808 })
1809 } else {
1810 None
1811 }
1812 })
1813 .collect(),
1814 )
1815 }
1816
1817 fn calculate_indent_guide_bounds(
1818 row_range: Range<MultiBufferRow>,
1819 line_height: Pixels,
1820 snapshot: &DisplaySnapshot,
1821 ) -> (gpui::Pixels, gpui::Pixels) {
1822 let start_point = Point::new(row_range.start.0, 0);
1823 let end_point = Point::new(row_range.end.0, 0);
1824
1825 let row_range = start_point.to_display_point(snapshot).row()
1826 ..end_point.to_display_point(snapshot).row();
1827
1828 let mut prev_line = start_point;
1829 prev_line.row = prev_line.row.saturating_sub(1);
1830 let prev_line = prev_line.to_display_point(snapshot).row();
1831
1832 let mut cons_line = end_point;
1833 cons_line.row += 1;
1834 let cons_line = cons_line.to_display_point(snapshot).row();
1835
1836 let mut offset_y = row_range.start.0 as f32 * line_height;
1837 let mut length = (cons_line.0.saturating_sub(row_range.start.0)) as f32 * line_height;
1838
1839 // If we are at the end of the buffer, ensure that the indent guide extends to the end of the line.
1840 if row_range.end == cons_line {
1841 length += line_height;
1842 }
1843
1844 // If there is a block (e.g. diagnostic) in between the start of the indent guide and the line above,
1845 // we want to extend the indent guide to the start of the block.
1846 let mut block_height = 0;
1847 let mut block_offset = 0;
1848 let mut found_excerpt_header = false;
1849 for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
1850 if matches!(block, Block::ExcerptBoundary { .. }) {
1851 found_excerpt_header = true;
1852 break;
1853 }
1854 block_offset += block.height();
1855 block_height += block.height();
1856 }
1857 if !found_excerpt_header {
1858 offset_y -= block_offset as f32 * line_height;
1859 length += block_height as f32 * line_height;
1860 }
1861
1862 // If there is a block (e.g. diagnostic) at the end of an multibuffer excerpt,
1863 // we want to ensure that the indent guide stops before the excerpt header.
1864 let mut block_height = 0;
1865 let mut found_excerpt_header = false;
1866 for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
1867 if matches!(block, Block::ExcerptBoundary { .. }) {
1868 found_excerpt_header = true;
1869 }
1870 block_height += block.height();
1871 }
1872 if found_excerpt_header {
1873 length -= block_height as f32 * line_height;
1874 }
1875
1876 (offset_y, length)
1877 }
1878
1879 #[allow(clippy::too_many_arguments)]
1880 fn layout_run_indicators(
1881 &self,
1882 line_height: Pixels,
1883 range: Range<DisplayRow>,
1884 scroll_pixel_position: gpui::Point<Pixels>,
1885 gutter_dimensions: &GutterDimensions,
1886 gutter_hitbox: &Hitbox,
1887 rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
1888 snapshot: &EditorSnapshot,
1889 window: &mut Window,
1890 cx: &mut App,
1891 ) -> Vec<AnyElement> {
1892 self.editor.update(cx, |editor, cx| {
1893 let active_task_indicator_row =
1894 if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
1895 deployed_from_indicator,
1896 actions,
1897 ..
1898 })) = editor.context_menu.borrow().as_ref()
1899 {
1900 actions
1901 .tasks
1902 .as_ref()
1903 .map(|tasks| tasks.position.to_display_point(snapshot).row())
1904 .or(*deployed_from_indicator)
1905 } else {
1906 None
1907 };
1908
1909 let offset_range_start = snapshot
1910 .display_point_to_anchor(DisplayPoint::new(range.start, 0), Bias::Left)
1911 .to_offset(&snapshot.buffer_snapshot);
1912 let offset_range_end = snapshot
1913 .display_point_to_anchor(DisplayPoint::new(range.end, 0), Bias::Right)
1914 .to_offset(&snapshot.buffer_snapshot);
1915
1916 editor
1917 .tasks
1918 .iter()
1919 .filter_map(|(_, tasks)| {
1920 if tasks.offset.0 < offset_range_start || tasks.offset.0 >= offset_range_end {
1921 return None;
1922 }
1923 let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
1924 let multibuffer_row = MultiBufferRow(multibuffer_point.row);
1925 let buffer_folded = snapshot
1926 .buffer_snapshot
1927 .buffer_line_for_row(multibuffer_row)
1928 .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
1929 .map(|buffer_id| editor.is_buffer_folded(buffer_id, cx))
1930 .unwrap_or(false);
1931 if buffer_folded {
1932 return None;
1933 }
1934
1935 if snapshot.is_line_folded(multibuffer_row) {
1936 // Skip folded indicators, unless it's the starting line of a fold.
1937 if multibuffer_row
1938 .0
1939 .checked_sub(1)
1940 .map_or(false, |previous_row| {
1941 snapshot.is_line_folded(MultiBufferRow(previous_row))
1942 })
1943 {
1944 return None;
1945 }
1946 }
1947 let display_row = multibuffer_point.to_display_point(snapshot).row();
1948 let button = editor.render_run_indicator(
1949 &self.style,
1950 Some(display_row) == active_task_indicator_row,
1951 display_row,
1952 cx,
1953 );
1954
1955 let button = prepaint_gutter_button(
1956 button,
1957 display_row,
1958 line_height,
1959 gutter_dimensions,
1960 scroll_pixel_position,
1961 gutter_hitbox,
1962 rows_with_hunk_bounds,
1963 window,
1964 cx,
1965 );
1966 Some(button)
1967 })
1968 .collect_vec()
1969 })
1970 }
1971
1972 #[allow(clippy::too_many_arguments)]
1973 fn layout_code_actions_indicator(
1974 &self,
1975 line_height: Pixels,
1976 newest_selection_head: DisplayPoint,
1977 scroll_pixel_position: gpui::Point<Pixels>,
1978 gutter_dimensions: &GutterDimensions,
1979 gutter_hitbox: &Hitbox,
1980 rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
1981 window: &mut Window,
1982 cx: &mut App,
1983 ) -> Option<AnyElement> {
1984 let mut active = false;
1985 let mut button = None;
1986 let row = newest_selection_head.row();
1987 self.editor.update(cx, |editor, cx| {
1988 if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
1989 deployed_from_indicator,
1990 ..
1991 })) = editor.context_menu.borrow().as_ref()
1992 {
1993 active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
1994 };
1995 button = editor.render_code_actions_indicator(&self.style, row, active, cx);
1996 });
1997
1998 let button = prepaint_gutter_button(
1999 button?,
2000 row,
2001 line_height,
2002 gutter_dimensions,
2003 scroll_pixel_position,
2004 gutter_hitbox,
2005 rows_with_hunk_bounds,
2006 window,
2007 cx,
2008 );
2009
2010 Some(button)
2011 }
2012
2013 fn get_participant_color(participant_index: Option<ParticipantIndex>, cx: &App) -> PlayerColor {
2014 if let Some(index) = participant_index {
2015 cx.theme().players().color_for_participant(index.0)
2016 } else {
2017 cx.theme().players().absent()
2018 }
2019 }
2020
2021 fn calculate_relative_line_numbers(
2022 &self,
2023 snapshot: &EditorSnapshot,
2024 rows: &Range<DisplayRow>,
2025 relative_to: Option<DisplayRow>,
2026 ) -> HashMap<DisplayRow, DisplayRowDelta> {
2027 let mut relative_rows: HashMap<DisplayRow, DisplayRowDelta> = Default::default();
2028 let Some(relative_to) = relative_to else {
2029 return relative_rows;
2030 };
2031
2032 let start = rows.start.min(relative_to);
2033 let end = rows.end.max(relative_to);
2034
2035 let buffer_rows = snapshot
2036 .row_infos(start)
2037 .take(1 + end.minus(start) as usize)
2038 .collect::<Vec<_>>();
2039
2040 let head_idx = relative_to.minus(start);
2041 let mut delta = 1;
2042 let mut i = head_idx + 1;
2043 while i < buffer_rows.len() as u32 {
2044 if buffer_rows[i as usize].buffer_row.is_some() {
2045 if rows.contains(&DisplayRow(i + start.0)) {
2046 relative_rows.insert(DisplayRow(i + start.0), delta);
2047 }
2048 delta += 1;
2049 }
2050 i += 1;
2051 }
2052 delta = 1;
2053 i = head_idx.min(buffer_rows.len() as u32 - 1);
2054 while i > 0 && buffer_rows[i as usize].buffer_row.is_none() {
2055 i -= 1;
2056 }
2057
2058 while i > 0 {
2059 i -= 1;
2060 if buffer_rows[i as usize].buffer_row.is_some() {
2061 if rows.contains(&DisplayRow(i + start.0)) {
2062 relative_rows.insert(DisplayRow(i + start.0), delta);
2063 }
2064 delta += 1;
2065 }
2066 }
2067
2068 relative_rows
2069 }
2070
2071 #[allow(clippy::too_many_arguments)]
2072 fn layout_line_numbers(
2073 &self,
2074 gutter_hitbox: Option<&Hitbox>,
2075 gutter_dimensions: GutterDimensions,
2076 line_height: Pixels,
2077 scroll_position: gpui::Point<f32>,
2078 rows: Range<DisplayRow>,
2079 buffer_rows: &[RowInfo],
2080 newest_selection_head: Option<DisplayPoint>,
2081 snapshot: &EditorSnapshot,
2082 window: &mut Window,
2083 cx: &mut App,
2084 ) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> {
2085 let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
2086 EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full
2087 });
2088 if !include_line_numbers {
2089 return Arc::default();
2090 }
2091
2092 let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| {
2093 let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
2094 let newest = editor.selections.newest::<Point>(cx);
2095 SelectionLayout::new(
2096 newest,
2097 editor.selections.line_mode,
2098 editor.cursor_shape,
2099 &snapshot.display_snapshot,
2100 true,
2101 true,
2102 None,
2103 )
2104 .head
2105 });
2106 let is_relative = editor.should_use_relative_line_numbers(cx);
2107 (newest_selection_head, is_relative)
2108 });
2109
2110 let relative_to = if is_relative {
2111 Some(newest_selection_head.row())
2112 } else {
2113 None
2114 };
2115 let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
2116 let mut line_number = String::new();
2117 let line_numbers = buffer_rows
2118 .into_iter()
2119 .enumerate()
2120 .flat_map(|(ix, row_info)| {
2121 let display_row = DisplayRow(rows.start.0 + ix as u32);
2122 line_number.clear();
2123 let non_relative_number = row_info.buffer_row? + 1;
2124 let number = relative_rows
2125 .get(&display_row)
2126 .unwrap_or(&non_relative_number);
2127 write!(&mut line_number, "{number}").unwrap();
2128 if row_info
2129 .diff_status
2130 .is_some_and(|status| status.is_deleted())
2131 {
2132 return None;
2133 }
2134
2135 let color = cx.theme().colors().editor_line_number;
2136 let shaped_line = self
2137 .shape_line_number(SharedString::from(&line_number), color, window)
2138 .log_err()?;
2139 let scroll_top = scroll_position.y * line_height;
2140 let line_origin = gutter_hitbox.map(|hitbox| {
2141 hitbox.origin
2142 + point(
2143 hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
2144 ix as f32 * line_height - (scroll_top % line_height),
2145 )
2146 });
2147
2148 #[cfg(not(test))]
2149 let hitbox = line_origin.map(|line_origin| {
2150 window.insert_hitbox(
2151 Bounds::new(line_origin, size(shaped_line.width, line_height)),
2152 false,
2153 )
2154 });
2155 #[cfg(test)]
2156 let hitbox = {
2157 let _ = line_origin;
2158 None
2159 };
2160
2161 let multi_buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
2162 let multi_buffer_row = MultiBufferRow(multi_buffer_row);
2163 let line_number = LineNumberLayout {
2164 shaped_line,
2165 hitbox,
2166 display_row,
2167 };
2168 Some((multi_buffer_row, line_number))
2169 })
2170 .collect();
2171 Arc::new(line_numbers)
2172 }
2173
2174 fn layout_crease_toggles(
2175 &self,
2176 rows: Range<DisplayRow>,
2177 row_infos: &[RowInfo],
2178 active_rows: &BTreeMap<DisplayRow, bool>,
2179 snapshot: &EditorSnapshot,
2180 window: &mut Window,
2181 cx: &mut App,
2182 ) -> Vec<Option<AnyElement>> {
2183 let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
2184 && snapshot.mode == EditorMode::Full
2185 && self.editor.read(cx).is_singleton(cx);
2186 if include_fold_statuses {
2187 row_infos
2188 .into_iter()
2189 .enumerate()
2190 .map(|(ix, info)| {
2191 let row = info.multibuffer_row?;
2192 let display_row = DisplayRow(rows.start.0 + ix as u32);
2193 let active = active_rows.contains_key(&display_row);
2194
2195 snapshot.render_crease_toggle(row, active, self.editor.clone(), window, cx)
2196 })
2197 .collect()
2198 } else {
2199 Vec::new()
2200 }
2201 }
2202
2203 fn layout_crease_trailers(
2204 &self,
2205 buffer_rows: impl IntoIterator<Item = RowInfo>,
2206 snapshot: &EditorSnapshot,
2207 window: &mut Window,
2208 cx: &mut App,
2209 ) -> Vec<Option<AnyElement>> {
2210 buffer_rows
2211 .into_iter()
2212 .map(|row_info| {
2213 if let Some(row) = row_info.multibuffer_row {
2214 snapshot.render_crease_trailer(row, window, cx)
2215 } else {
2216 None
2217 }
2218 })
2219 .collect()
2220 }
2221
2222 fn layout_lines(
2223 rows: Range<DisplayRow>,
2224 snapshot: &EditorSnapshot,
2225 style: &EditorStyle,
2226 editor_width: Pixels,
2227 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
2228 window: &mut Window,
2229 cx: &mut App,
2230 ) -> Vec<LineWithInvisibles> {
2231 if rows.start >= rows.end {
2232 return Vec::new();
2233 }
2234
2235 // Show the placeholder when the editor is empty
2236 if snapshot.is_empty() {
2237 let font_size = style.text.font_size.to_pixels(window.rem_size());
2238 let placeholder_color = cx.theme().colors().text_placeholder;
2239 let placeholder_text = snapshot.placeholder_text();
2240
2241 let placeholder_lines = placeholder_text
2242 .as_ref()
2243 .map_or("", AsRef::as_ref)
2244 .split('\n')
2245 .skip(rows.start.0 as usize)
2246 .chain(iter::repeat(""))
2247 .take(rows.len());
2248 placeholder_lines
2249 .filter_map(move |line| {
2250 let run = TextRun {
2251 len: line.len(),
2252 font: style.text.font(),
2253 color: placeholder_color,
2254 background_color: None,
2255 underline: Default::default(),
2256 strikethrough: None,
2257 };
2258 window
2259 .text_system()
2260 .shape_line(line.to_string().into(), font_size, &[run])
2261 .log_err()
2262 })
2263 .map(|line| LineWithInvisibles {
2264 width: line.width,
2265 len: line.len,
2266 fragments: smallvec![LineFragment::Text(line)],
2267 invisibles: Vec::new(),
2268 font_size,
2269 })
2270 .collect()
2271 } else {
2272 let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
2273 LineWithInvisibles::from_chunks(
2274 chunks,
2275 &style,
2276 MAX_LINE_LEN,
2277 rows.len(),
2278 snapshot.mode,
2279 editor_width,
2280 is_row_soft_wrapped,
2281 window,
2282 cx,
2283 )
2284 }
2285 }
2286
2287 #[allow(clippy::too_many_arguments)]
2288 fn prepaint_lines(
2289 &self,
2290 start_row: DisplayRow,
2291 line_layouts: &mut [LineWithInvisibles],
2292 line_height: Pixels,
2293 scroll_pixel_position: gpui::Point<Pixels>,
2294 content_origin: gpui::Point<Pixels>,
2295 window: &mut Window,
2296 cx: &mut App,
2297 ) -> SmallVec<[AnyElement; 1]> {
2298 let mut line_elements = SmallVec::new();
2299 for (ix, line) in line_layouts.iter_mut().enumerate() {
2300 let row = start_row + DisplayRow(ix as u32);
2301 line.prepaint(
2302 line_height,
2303 scroll_pixel_position,
2304 row,
2305 content_origin,
2306 &mut line_elements,
2307 window,
2308 cx,
2309 );
2310 }
2311 line_elements
2312 }
2313
2314 #[allow(clippy::too_many_arguments)]
2315 fn render_block(
2316 &self,
2317 block: &Block,
2318 available_width: AvailableSpace,
2319 block_id: BlockId,
2320 block_row_start: DisplayRow,
2321 snapshot: &EditorSnapshot,
2322 text_x: Pixels,
2323 rows: &Range<DisplayRow>,
2324 line_layouts: &[LineWithInvisibles],
2325 gutter_dimensions: &GutterDimensions,
2326 line_height: Pixels,
2327 em_width: Pixels,
2328 text_hitbox: &Hitbox,
2329 editor_width: Pixels,
2330 scroll_width: &mut Pixels,
2331 resized_blocks: &mut HashMap<CustomBlockId, u32>,
2332 selections: &[Selection<Point>],
2333 selected_buffer_ids: &Vec<BufferId>,
2334 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
2335 sticky_header_excerpt_id: Option<ExcerptId>,
2336 window: &mut Window,
2337 cx: &mut App,
2338 ) -> (AnyElement, Size<Pixels>) {
2339 let mut element = match block {
2340 Block::Custom(block) => {
2341 let block_start = block.start().to_point(&snapshot.buffer_snapshot);
2342 let block_end = block.end().to_point(&snapshot.buffer_snapshot);
2343 let align_to = block_start.to_display_point(snapshot);
2344 let anchor_x = text_x
2345 + if rows.contains(&align_to.row()) {
2346 line_layouts[align_to.row().minus(rows.start) as usize]
2347 .x_for_index(align_to.column() as usize)
2348 } else {
2349 layout_line(
2350 align_to.row(),
2351 snapshot,
2352 &self.style,
2353 editor_width,
2354 is_row_soft_wrapped,
2355 window,
2356 cx,
2357 )
2358 .x_for_index(align_to.column() as usize)
2359 };
2360
2361 let selected = selections
2362 .binary_search_by(|selection| {
2363 if selection.end <= block_start {
2364 Ordering::Less
2365 } else if selection.start >= block_end {
2366 Ordering::Greater
2367 } else {
2368 Ordering::Equal
2369 }
2370 })
2371 .is_ok();
2372
2373 div()
2374 .size_full()
2375 .child(block.render(&mut BlockContext {
2376 window,
2377 app: cx,
2378 anchor_x,
2379 gutter_dimensions,
2380 line_height,
2381 em_width,
2382 block_id,
2383 selected,
2384 max_width: text_hitbox.size.width.max(*scroll_width),
2385 editor_style: &self.style,
2386 }))
2387 .into_any()
2388 }
2389
2390 Block::FoldedBuffer {
2391 first_excerpt,
2392 prev_excerpt,
2393 show_excerpt_controls,
2394 height,
2395 } => {
2396 let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
2397 let mut result = v_flex().id(block_id).w_full();
2398
2399 if let Some(prev_excerpt) = prev_excerpt {
2400 if *show_excerpt_controls {
2401 result = result.child(self.render_expand_excerpt_control(
2402 block_id,
2403 ExpandExcerptDirection::Down,
2404 prev_excerpt.id,
2405 gutter_dimensions,
2406 window,
2407 cx,
2408 ));
2409 }
2410 }
2411
2412 let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt);
2413 result
2414 .child(self.render_buffer_header(
2415 first_excerpt,
2416 true,
2417 selected,
2418 jump_data,
2419 window,
2420 cx,
2421 ))
2422 .into_any_element()
2423 }
2424
2425 Block::ExcerptBoundary {
2426 prev_excerpt,
2427 next_excerpt,
2428 show_excerpt_controls,
2429 height,
2430 starts_new_buffer,
2431 } => {
2432 let color = cx.theme().colors().clone();
2433 let mut result = v_flex().id(block_id).w_full();
2434
2435 if let Some(prev_excerpt) = prev_excerpt {
2436 if *show_excerpt_controls {
2437 result = result.child(self.render_expand_excerpt_control(
2438 block_id,
2439 ExpandExcerptDirection::Down,
2440 prev_excerpt.id,
2441 gutter_dimensions,
2442 window,
2443 cx,
2444 ));
2445 }
2446 }
2447
2448 if let Some(next_excerpt) = next_excerpt {
2449 let jump_data =
2450 header_jump_data(snapshot, block_row_start, *height, next_excerpt);
2451
2452 if *starts_new_buffer {
2453 if sticky_header_excerpt_id != Some(next_excerpt.id) {
2454 let selected = selected_buffer_ids.contains(&next_excerpt.buffer_id);
2455
2456 result = result.child(self.render_buffer_header(
2457 next_excerpt,
2458 false,
2459 selected,
2460 jump_data,
2461 window,
2462 cx,
2463 ));
2464 } else {
2465 result = result
2466 .child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
2467 }
2468
2469 if *show_excerpt_controls {
2470 result = result.child(self.render_expand_excerpt_control(
2471 block_id,
2472 ExpandExcerptDirection::Up,
2473 next_excerpt.id,
2474 gutter_dimensions,
2475 window,
2476 cx,
2477 ));
2478 }
2479 } else {
2480 if *show_excerpt_controls {
2481 result = result.child(
2482 h_flex()
2483 .relative()
2484 .child(
2485 div()
2486 .top(px(0.))
2487 .absolute()
2488 .w_full()
2489 .h_px()
2490 .bg(color.border_variant),
2491 )
2492 .child(self.render_expand_excerpt_control(
2493 block_id,
2494 ExpandExcerptDirection::Up,
2495 next_excerpt.id,
2496 gutter_dimensions,
2497 window,
2498 cx,
2499 )),
2500 );
2501 }
2502 };
2503 }
2504
2505 result.into_any()
2506 }
2507 };
2508
2509 // Discover the element's content height, then round up to the nearest multiple of line height.
2510 let preliminary_size = element.layout_as_root(
2511 size(available_width, AvailableSpace::MinContent),
2512 window,
2513 cx,
2514 );
2515 let quantized_height = (preliminary_size.height / line_height).ceil() * line_height;
2516 let final_size = if preliminary_size.height == quantized_height {
2517 preliminary_size
2518 } else {
2519 element.layout_as_root(size(available_width, quantized_height.into()), window, cx)
2520 };
2521
2522 if let BlockId::Custom(custom_block_id) = block_id {
2523 if block.height() > 0 {
2524 let element_height_in_lines =
2525 ((final_size.height / line_height).ceil() as u32).max(1);
2526 if element_height_in_lines != block.height() {
2527 resized_blocks.insert(custom_block_id, element_height_in_lines);
2528 }
2529 }
2530 }
2531
2532 (element, final_size)
2533 }
2534
2535 fn render_buffer_header(
2536 &self,
2537 for_excerpt: &ExcerptInfo,
2538 is_folded: bool,
2539 is_selected: bool,
2540 jump_data: JumpData,
2541 window: &mut Window,
2542 cx: &mut App,
2543 ) -> Div {
2544 let include_root = self
2545 .editor
2546 .read(cx)
2547 .project
2548 .as_ref()
2549 .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
2550 .unwrap_or_default();
2551 let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
2552 let filename = path
2553 .as_ref()
2554 .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
2555 let parent_path = path.as_ref().and_then(|path| {
2556 Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
2557 });
2558 let focus_handle = self.editor.focus_handle(cx);
2559 let colors = cx.theme().colors();
2560
2561 div()
2562 .px_2()
2563 .pt_2()
2564 .w_full()
2565 .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
2566 .child(
2567 h_flex()
2568 .size_full()
2569 .gap_2()
2570 .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
2571 .pl_0p5()
2572 .pr_5()
2573 .rounded_md()
2574 .shadow_md()
2575 .border_1()
2576 .map(|div| {
2577 let border_color = if is_selected && is_folded {
2578 colors.border_focused
2579 } else {
2580 colors.border
2581 };
2582 div.border_color(border_color)
2583 })
2584 .bg(colors.editor_subheader_background)
2585 .hover(|style| style.bg(colors.element_hover))
2586 .map(|header| {
2587 let editor = self.editor.clone();
2588 let buffer_id = for_excerpt.buffer_id;
2589 let toggle_chevron_icon =
2590 FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
2591 header.child(
2592 div()
2593 .hover(|style| style.bg(colors.element_selected))
2594 .rounded_sm()
2595 .child(
2596 ButtonLike::new("toggle-buffer-fold")
2597 .style(ui::ButtonStyle::Transparent)
2598 .size(ButtonSize::Large)
2599 .width(px(30.).into())
2600 .children(toggle_chevron_icon)
2601 .tooltip({
2602 let focus_handle = focus_handle.clone();
2603 move |window, cx| {
2604 Tooltip::for_action_in(
2605 "Toggle Excerpt Fold",
2606 &ToggleFold,
2607 &focus_handle,
2608 window,
2609 cx,
2610 )
2611 }
2612 })
2613 .on_click(move |_, _, cx| {
2614 if is_folded {
2615 editor.update(cx, |editor, cx| {
2616 editor.unfold_buffer(buffer_id, cx);
2617 });
2618 } else {
2619 editor.update(cx, |editor, cx| {
2620 editor.fold_buffer(buffer_id, cx);
2621 });
2622 }
2623 }),
2624 ),
2625 )
2626 })
2627 .children(
2628 self.editor
2629 .read(cx)
2630 .addons
2631 .values()
2632 .filter_map(|addon| {
2633 addon.render_buffer_header_controls(for_excerpt, window, cx)
2634 })
2635 .take(1),
2636 )
2637 .child(
2638 h_flex()
2639 .cursor_pointer()
2640 .id("path header block")
2641 .size_full()
2642 .justify_between()
2643 .child(
2644 h_flex()
2645 .gap_2()
2646 .child(
2647 filename
2648 .map(SharedString::from)
2649 .unwrap_or_else(|| "untitled".into()),
2650 )
2651 .when_some(parent_path, |then, path| {
2652 then.child(div().child(path).text_color(colors.text_muted))
2653 }),
2654 )
2655 .when(is_selected, |el| {
2656 el.child(
2657 h_flex()
2658 .id("jump-to-file-button")
2659 .gap_2p5()
2660 .child(Label::new("Jump To File"))
2661 .children(
2662 KeyBinding::for_action_in(
2663 &OpenExcerpts,
2664 &focus_handle,
2665 window,
2666 cx,
2667 )
2668 .map(|binding| binding.into_any_element()),
2669 ),
2670 )
2671 })
2672 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
2673 .on_click(window.listener_for(&self.editor, {
2674 move |editor, e: &ClickEvent, window, cx| {
2675 editor.open_excerpts_common(
2676 Some(jump_data.clone()),
2677 e.down.modifiers.secondary(),
2678 window,
2679 cx,
2680 );
2681 }
2682 })),
2683 ),
2684 )
2685 }
2686
2687 fn render_expand_excerpt_control(
2688 &self,
2689 block_id: BlockId,
2690 direction: ExpandExcerptDirection,
2691 excerpt_id: ExcerptId,
2692 gutter_dimensions: &GutterDimensions,
2693 window: &Window,
2694 cx: &mut App,
2695 ) -> impl IntoElement {
2696 let color = cx.theme().colors().clone();
2697 let hover_color = color.border_variant.opacity(0.5);
2698 let focus_handle = self.editor.focus_handle(cx).clone();
2699
2700 let icon_offset =
2701 gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin);
2702 let header_height = MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * window.line_height();
2703 let group_name = if direction == ExpandExcerptDirection::Down {
2704 "expand-down"
2705 } else {
2706 "expand-up"
2707 };
2708
2709 let expand_area = |id: SharedString| {
2710 h_flex()
2711 .id(id)
2712 .w_full()
2713 .cursor_pointer()
2714 .block_mouse_down()
2715 .on_mouse_move(|_, _, cx| cx.stop_propagation())
2716 .hover(|style| style.bg(hover_color))
2717 .tooltip({
2718 let focus_handle = focus_handle.clone();
2719 move |window, cx| {
2720 Tooltip::for_action_in(
2721 "Expand Excerpt",
2722 &ExpandExcerpts { lines: 0 },
2723 &focus_handle,
2724 window,
2725 cx,
2726 )
2727 }
2728 })
2729 };
2730
2731 expand_area(
2732 format!(
2733 "block-{}-{}",
2734 block_id,
2735 if direction == ExpandExcerptDirection::Down {
2736 "down"
2737 } else {
2738 "up"
2739 }
2740 )
2741 .into(),
2742 )
2743 .group(group_name)
2744 .child(
2745 h_flex()
2746 .w(icon_offset)
2747 .h(header_height)
2748 .flex_none()
2749 .justify_end()
2750 .child(
2751 ButtonLike::new("expand-icon")
2752 .style(ButtonStyle::Transparent)
2753 .child(
2754 svg()
2755 .path(if direction == ExpandExcerptDirection::Down {
2756 IconName::ArrowDownFromLine.path()
2757 } else {
2758 IconName::ArrowUpFromLine.path()
2759 })
2760 .size(IconSize::XSmall.rems())
2761 .text_color(cx.theme().colors().editor_line_number)
2762 .group_hover(group_name, |style| {
2763 style.text_color(cx.theme().colors().editor_active_line_number)
2764 }),
2765 ),
2766 ),
2767 )
2768 .on_click(window.listener_for(&self.editor, {
2769 move |editor, _, _, cx| {
2770 editor.expand_excerpt(excerpt_id, direction, cx);
2771 cx.stop_propagation();
2772 }
2773 }))
2774 }
2775
2776 #[allow(clippy::too_many_arguments)]
2777 fn render_blocks(
2778 &self,
2779 rows: Range<DisplayRow>,
2780 snapshot: &EditorSnapshot,
2781 hitbox: &Hitbox,
2782 text_hitbox: &Hitbox,
2783 editor_width: Pixels,
2784 scroll_width: &mut Pixels,
2785 gutter_dimensions: &GutterDimensions,
2786 em_width: Pixels,
2787 text_x: Pixels,
2788 line_height: Pixels,
2789 line_layouts: &[LineWithInvisibles],
2790 selections: &[Selection<Point>],
2791 selected_buffer_ids: &Vec<BufferId>,
2792 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
2793 sticky_header_excerpt_id: Option<ExcerptId>,
2794 window: &mut Window,
2795 cx: &mut App,
2796 ) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
2797 let (fixed_blocks, non_fixed_blocks) = snapshot
2798 .blocks_in_range(rows.clone())
2799 .partition::<Vec<_>, _>(|(_, block)| block.style() == BlockStyle::Fixed);
2800
2801 let mut focused_block = self
2802 .editor
2803 .update(cx, |editor, _| editor.take_focused_block());
2804 let mut fixed_block_max_width = Pixels::ZERO;
2805 let mut blocks = Vec::new();
2806 let mut resized_blocks = HashMap::default();
2807
2808 for (row, block) in fixed_blocks {
2809 let block_id = block.id();
2810
2811 if focused_block.as_ref().map_or(false, |b| b.id == block_id) {
2812 focused_block = None;
2813 }
2814
2815 let (element, element_size) = self.render_block(
2816 block,
2817 AvailableSpace::MinContent,
2818 block_id,
2819 row,
2820 snapshot,
2821 text_x,
2822 &rows,
2823 line_layouts,
2824 gutter_dimensions,
2825 line_height,
2826 em_width,
2827 text_hitbox,
2828 editor_width,
2829 scroll_width,
2830 &mut resized_blocks,
2831 selections,
2832 selected_buffer_ids,
2833 is_row_soft_wrapped,
2834 sticky_header_excerpt_id,
2835 window,
2836 cx,
2837 );
2838 fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
2839 blocks.push(BlockLayout {
2840 id: block_id,
2841 row: Some(row),
2842 element,
2843 available_space: size(AvailableSpace::MinContent, element_size.height.into()),
2844 style: BlockStyle::Fixed,
2845 });
2846 }
2847
2848 for (row, block) in non_fixed_blocks {
2849 let style = block.style();
2850 let width = match style {
2851 BlockStyle::Sticky => hitbox.size.width,
2852 BlockStyle::Flex => hitbox
2853 .size
2854 .width
2855 .max(fixed_block_max_width)
2856 .max(gutter_dimensions.width + *scroll_width),
2857 BlockStyle::Fixed => unreachable!(),
2858 };
2859 let block_id = block.id();
2860
2861 if focused_block.as_ref().map_or(false, |b| b.id == block_id) {
2862 focused_block = None;
2863 }
2864
2865 let (element, element_size) = self.render_block(
2866 block,
2867 width.into(),
2868 block_id,
2869 row,
2870 snapshot,
2871 text_x,
2872 &rows,
2873 line_layouts,
2874 gutter_dimensions,
2875 line_height,
2876 em_width,
2877 text_hitbox,
2878 editor_width,
2879 scroll_width,
2880 &mut resized_blocks,
2881 selections,
2882 selected_buffer_ids,
2883 is_row_soft_wrapped,
2884 sticky_header_excerpt_id,
2885 window,
2886 cx,
2887 );
2888
2889 blocks.push(BlockLayout {
2890 id: block_id,
2891 row: Some(row),
2892 element,
2893 available_space: size(width.into(), element_size.height.into()),
2894 style,
2895 });
2896 }
2897
2898 if let Some(focused_block) = focused_block {
2899 if let Some(focus_handle) = focused_block.focus_handle.upgrade() {
2900 if focus_handle.is_focused(window) {
2901 if let Some(block) = snapshot.block_for_id(focused_block.id) {
2902 let style = block.style();
2903 let width = match style {
2904 BlockStyle::Fixed => AvailableSpace::MinContent,
2905 BlockStyle::Flex => AvailableSpace::Definite(
2906 hitbox
2907 .size
2908 .width
2909 .max(fixed_block_max_width)
2910 .max(gutter_dimensions.width + *scroll_width),
2911 ),
2912 BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
2913 };
2914
2915 let (element, element_size) = self.render_block(
2916 &block,
2917 width,
2918 focused_block.id,
2919 rows.end,
2920 snapshot,
2921 text_x,
2922 &rows,
2923 line_layouts,
2924 gutter_dimensions,
2925 line_height,
2926 em_width,
2927 text_hitbox,
2928 editor_width,
2929 scroll_width,
2930 &mut resized_blocks,
2931 selections,
2932 selected_buffer_ids,
2933 is_row_soft_wrapped,
2934 sticky_header_excerpt_id,
2935 window,
2936 cx,
2937 );
2938
2939 blocks.push(BlockLayout {
2940 id: block.id(),
2941 row: None,
2942 element,
2943 available_space: size(width, element_size.height.into()),
2944 style,
2945 });
2946 }
2947 }
2948 }
2949 }
2950
2951 if resized_blocks.is_empty() {
2952 *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width);
2953 Ok(blocks)
2954 } else {
2955 Err(resized_blocks)
2956 }
2957 }
2958
2959 /// Returns true if any of the blocks changed size since the previous frame. This will trigger
2960 /// a restart of rendering for the editor based on the new sizes.
2961 #[allow(clippy::too_many_arguments)]
2962 fn layout_blocks(
2963 &self,
2964 blocks: &mut Vec<BlockLayout>,
2965 block_starts: &mut HashSet<DisplayRow>,
2966 hitbox: &Hitbox,
2967 line_height: Pixels,
2968 scroll_pixel_position: gpui::Point<Pixels>,
2969 window: &mut Window,
2970 cx: &mut App,
2971 ) {
2972 for block in blocks {
2973 let mut origin = if let Some(row) = block.row {
2974 block_starts.insert(row);
2975 hitbox.origin
2976 + point(
2977 Pixels::ZERO,
2978 row.as_f32() * line_height - scroll_pixel_position.y,
2979 )
2980 } else {
2981 // Position the block outside the visible area
2982 hitbox.origin + point(Pixels::ZERO, hitbox.size.height)
2983 };
2984
2985 if !matches!(block.style, BlockStyle::Sticky) {
2986 origin += point(-scroll_pixel_position.x, Pixels::ZERO);
2987 }
2988
2989 let focus_handle =
2990 block
2991 .element
2992 .prepaint_as_root(origin, block.available_space, window, cx);
2993
2994 if let Some(focus_handle) = focus_handle {
2995 self.editor.update(cx, |editor, _cx| {
2996 editor.set_focused_block(FocusedBlock {
2997 id: block.id,
2998 focus_handle: focus_handle.downgrade(),
2999 });
3000 });
3001 }
3002 }
3003 }
3004
3005 #[allow(clippy::too_many_arguments)]
3006 fn layout_sticky_buffer_header(
3007 &self,
3008 StickyHeaderExcerpt {
3009 excerpt,
3010 next_excerpt_controls_present,
3011 next_buffer_row,
3012 }: StickyHeaderExcerpt<'_>,
3013 scroll_position: f32,
3014 line_height: Pixels,
3015 snapshot: &EditorSnapshot,
3016 hitbox: &Hitbox,
3017 selected_buffer_ids: &Vec<BufferId>,
3018 window: &mut Window,
3019 cx: &mut App,
3020 ) -> AnyElement {
3021 let jump_data = header_jump_data(
3022 snapshot,
3023 DisplayRow(scroll_position as u32),
3024 FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
3025 excerpt,
3026 );
3027
3028 let editor_bg_color = cx.theme().colors().editor_background;
3029
3030 let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
3031
3032 let mut header = v_flex()
3033 .relative()
3034 .child(
3035 div()
3036 .w(hitbox.bounds.size.width)
3037 .h(FILE_HEADER_HEIGHT as f32 * line_height)
3038 .bg(linear_gradient(
3039 0.,
3040 linear_color_stop(editor_bg_color.opacity(0.), 0.),
3041 linear_color_stop(editor_bg_color, 0.6),
3042 ))
3043 .absolute()
3044 .top_0(),
3045 )
3046 .child(
3047 self.render_buffer_header(excerpt, false, selected, jump_data, window, cx)
3048 .into_any_element(),
3049 )
3050 .into_any_element();
3051
3052 let mut origin = hitbox.origin;
3053
3054 if let Some(next_buffer_row) = next_buffer_row {
3055 // Push up the sticky header when the excerpt is getting close to the top of the viewport
3056
3057 let mut max_row = next_buffer_row - FILE_HEADER_HEIGHT * 2;
3058
3059 if next_excerpt_controls_present {
3060 max_row -= MULTI_BUFFER_EXCERPT_HEADER_HEIGHT;
3061 }
3062
3063 let offset = scroll_position - max_row as f32;
3064
3065 if offset > 0.0 {
3066 origin.y -= Pixels(offset) * line_height;
3067 }
3068 }
3069
3070 let size = size(
3071 AvailableSpace::Definite(hitbox.size.width),
3072 AvailableSpace::MinContent,
3073 );
3074
3075 header.prepaint_as_root(origin, size, window, cx);
3076
3077 header
3078 }
3079
3080 #[allow(clippy::too_many_arguments)]
3081 fn layout_cursor_popovers(
3082 &self,
3083 line_height: Pixels,
3084 text_hitbox: &Hitbox,
3085 content_origin: gpui::Point<Pixels>,
3086 start_row: DisplayRow,
3087 scroll_pixel_position: gpui::Point<Pixels>,
3088 line_layouts: &[LineWithInvisibles],
3089 cursor: DisplayPoint,
3090 cursor_point: Point,
3091 style: &EditorStyle,
3092 window: &mut Window,
3093 cx: &mut App,
3094 ) {
3095 let mut min_menu_height = Pixels::ZERO;
3096 let mut max_menu_height = Pixels::ZERO;
3097 let mut height_above_menu = Pixels::ZERO;
3098 let height_below_menu = Pixels::ZERO;
3099 let mut edit_prediction_popover_visible = false;
3100 let mut context_menu_visible = false;
3101
3102 {
3103 let editor = self.editor.read(cx);
3104 if editor
3105 .edit_prediction_visible_in_cursor_popover(editor.has_active_inline_completion())
3106 {
3107 height_above_menu +=
3108 editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING;
3109 edit_prediction_popover_visible = true;
3110 }
3111
3112 if editor.context_menu_visible() {
3113 if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() {
3114 min_menu_height += line_height * 3. + POPOVER_Y_PADDING;
3115 max_menu_height += line_height * 12. + POPOVER_Y_PADDING;
3116 context_menu_visible = true;
3117 }
3118 }
3119 }
3120
3121 let visible = edit_prediction_popover_visible || context_menu_visible;
3122 if !visible {
3123 return;
3124 }
3125
3126 let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
3127 let target_position = content_origin
3128 + gpui::Point {
3129 x: cmp::max(
3130 px(0.),
3131 cursor_row_layout.x_for_index(cursor.column() as usize)
3132 - scroll_pixel_position.x,
3133 ),
3134 y: cmp::max(
3135 px(0.),
3136 cursor.row().next_row().as_f32() * line_height - scroll_pixel_position.y,
3137 ),
3138 };
3139
3140 let viewport_bounds =
3141 Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
3142 right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
3143 ..Default::default()
3144 });
3145
3146 let min_height = height_above_menu + min_menu_height + height_below_menu;
3147 let max_height = height_above_menu + max_menu_height + height_below_menu;
3148 let Some((laid_out_popovers, y_flipped)) = self.layout_popovers_above_or_below_line(
3149 target_position,
3150 line_height,
3151 min_height,
3152 max_height,
3153 text_hitbox,
3154 viewport_bounds,
3155 window,
3156 cx,
3157 |height, max_width_for_stable_x, y_flipped, window, cx| {
3158 // First layout the menu to get its size - others can be at least this wide.
3159 let context_menu = if context_menu_visible {
3160 let menu_height = if y_flipped {
3161 height - height_below_menu
3162 } else {
3163 height - height_above_menu
3164 };
3165 let mut element = self
3166 .render_context_menu(line_height, menu_height, y_flipped, window, cx)
3167 .expect("Visible context menu should always render.");
3168 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3169 Some((CursorPopoverType::CodeContextMenu, element, size))
3170 } else {
3171 None
3172 };
3173 let min_width = context_menu
3174 .as_ref()
3175 .map_or(px(0.), |(_, _, size)| size.width);
3176 let max_width = max_width_for_stable_x.max(
3177 context_menu
3178 .as_ref()
3179 .map_or(px(0.), |(_, _, size)| size.width),
3180 );
3181
3182 let edit_prediction = if edit_prediction_popover_visible {
3183 self.editor.update(cx, move |editor, cx| {
3184 let accept_binding = editor.accept_edit_prediction_keybind(window, cx);
3185 let mut element = editor.render_edit_prediction_cursor_popover(
3186 min_width,
3187 max_width,
3188 cursor_point,
3189 style,
3190 accept_binding.keystroke(),
3191 window,
3192 cx,
3193 )?;
3194 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3195 Some((CursorPopoverType::EditPrediction, element, size))
3196 })
3197 } else {
3198 None
3199 };
3200 vec![edit_prediction, context_menu]
3201 .into_iter()
3202 .flatten()
3203 .collect::<Vec<_>>()
3204 },
3205 ) else {
3206 return;
3207 };
3208
3209 let Some((menu_ix, (_, menu_bounds))) = laid_out_popovers
3210 .iter()
3211 .find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))
3212 else {
3213 return;
3214 };
3215 let last_ix = laid_out_popovers.len() - 1;
3216 let menu_is_last = menu_ix == last_ix;
3217 let first_popover_bounds = laid_out_popovers[0].1;
3218 let last_popover_bounds = laid_out_popovers[last_ix].1;
3219
3220 // Bounds to layout the aside around. When y_flipped, the aside goes either above or to the
3221 // right, and otherwise it goes below or to the right.
3222 let mut target_bounds = Bounds::from_corners(
3223 first_popover_bounds.origin,
3224 last_popover_bounds.bottom_right(),
3225 );
3226 target_bounds.size.width = menu_bounds.size.width;
3227
3228 // Like `target_bounds`, but with the max height it could occupy. Choosing an aside position
3229 // based on this is preferred for layout stability.
3230 let mut max_target_bounds = target_bounds;
3231 max_target_bounds.size.height = max_height;
3232 if y_flipped {
3233 max_target_bounds.origin.y -= max_height - target_bounds.size.height;
3234 }
3235
3236 // Add spacing around `target_bounds` and `max_target_bounds`.
3237 let mut extend_amount = Edges::all(MENU_GAP);
3238 if y_flipped {
3239 extend_amount.bottom = line_height;
3240 } else {
3241 extend_amount.top = line_height;
3242 }
3243 let target_bounds = target_bounds.extend(extend_amount);
3244 let max_target_bounds = max_target_bounds.extend(extend_amount);
3245
3246 let must_place_above_or_below =
3247 if y_flipped && !menu_is_last && menu_bounds.size.height < max_menu_height {
3248 laid_out_popovers[menu_ix + 1..]
3249 .iter()
3250 .any(|(_, popover_bounds)| popover_bounds.size.width > menu_bounds.size.width)
3251 } else {
3252 false
3253 };
3254
3255 self.layout_context_menu_aside(
3256 y_flipped,
3257 *menu_bounds,
3258 target_bounds,
3259 max_target_bounds,
3260 max_menu_height,
3261 must_place_above_or_below,
3262 text_hitbox,
3263 viewport_bounds,
3264 window,
3265 cx,
3266 );
3267 }
3268
3269 #[allow(clippy::too_many_arguments)]
3270 fn layout_gutter_menu(
3271 &self,
3272 line_height: Pixels,
3273 text_hitbox: &Hitbox,
3274 content_origin: gpui::Point<Pixels>,
3275 scroll_pixel_position: gpui::Point<Pixels>,
3276 gutter_overshoot: Pixels,
3277 window: &mut Window,
3278 cx: &mut App,
3279 ) {
3280 let editor = self.editor.read(cx);
3281 if !editor.context_menu_visible() {
3282 return;
3283 }
3284 let Some(crate::ContextMenuOrigin::GutterIndicator(gutter_row)) =
3285 editor.context_menu_origin()
3286 else {
3287 return;
3288 };
3289 // Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the
3290 // indicator than just a plain first column of the text field.
3291 let target_position = content_origin
3292 + gpui::Point {
3293 x: -gutter_overshoot,
3294 y: gutter_row.next_row().as_f32() * line_height - scroll_pixel_position.y,
3295 };
3296 let min_height = line_height * 3. + POPOVER_Y_PADDING;
3297 let max_height = line_height * 12. + POPOVER_Y_PADDING;
3298 let viewport_bounds =
3299 Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
3300 right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
3301 ..Default::default()
3302 });
3303 self.layout_popovers_above_or_below_line(
3304 target_position,
3305 line_height,
3306 min_height,
3307 max_height,
3308 text_hitbox,
3309 viewport_bounds,
3310 window,
3311 cx,
3312 move |height, _max_width_for_stable_x, y_flipped, window, cx| {
3313 let mut element = self
3314 .render_context_menu(line_height, height, y_flipped, window, cx)
3315 .expect("Visible context menu should always render.");
3316 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3317 vec![(CursorPopoverType::CodeContextMenu, element, size)]
3318 },
3319 );
3320 }
3321
3322 #[allow(clippy::too_many_arguments)]
3323 fn layout_popovers_above_or_below_line(
3324 &self,
3325 target_position: gpui::Point<Pixels>,
3326 line_height: Pixels,
3327 min_height: Pixels,
3328 max_height: Pixels,
3329 text_hitbox: &Hitbox,
3330 viewport_bounds: Bounds<Pixels>,
3331 window: &mut Window,
3332 cx: &mut App,
3333 make_sized_popovers: impl FnOnce(
3334 Pixels,
3335 Pixels,
3336 bool,
3337 &mut Window,
3338 &mut App,
3339 ) -> Vec<(CursorPopoverType, AnyElement, Size<Pixels>)>,
3340 ) -> Option<(Vec<(CursorPopoverType, Bounds<Pixels>)>, bool)> {
3341 let text_style = TextStyleRefinement {
3342 line_height: Some(DefiniteLength::Fraction(
3343 BufferLineHeight::Comfortable.value(),
3344 )),
3345 ..Default::default()
3346 };
3347 window.with_text_style(Some(text_style), |window| {
3348 // If the max height won't fit below and there is more space above, put it above the line.
3349 let bottom_y_when_flipped = target_position.y - line_height;
3350 let available_above = bottom_y_when_flipped - text_hitbox.top();
3351 let available_below = text_hitbox.bottom() - target_position.y;
3352 let y_overflows_below = max_height > available_below;
3353 let mut y_flipped = y_overflows_below && available_above > available_below;
3354 let mut height = cmp::min(
3355 max_height,
3356 if y_flipped {
3357 available_above
3358 } else {
3359 available_below
3360 },
3361 );
3362
3363 // If the min height doesn't fit within text bounds, instead fit within the window.
3364 if height < min_height {
3365 let available_above = bottom_y_when_flipped;
3366 let available_below = viewport_bounds.bottom() - target_position.y;
3367 if available_below > min_height {
3368 y_flipped = false;
3369 height = min_height;
3370 } else if available_above > min_height {
3371 y_flipped = true;
3372 height = min_height;
3373 } else if available_above > available_below {
3374 y_flipped = true;
3375 height = available_above;
3376 } else {
3377 y_flipped = false;
3378 height = available_below;
3379 }
3380 }
3381
3382 let max_width_for_stable_x = viewport_bounds.right() - target_position.x;
3383
3384 // TODO: Use viewport_bounds.width as a max width so that it doesn't get clipped on the left
3385 // for very narrow windows.
3386 let popovers =
3387 make_sized_popovers(height, max_width_for_stable_x, y_flipped, window, cx);
3388 if popovers.is_empty() {
3389 return None;
3390 }
3391
3392 let max_width = popovers
3393 .iter()
3394 .map(|(_, _, size)| size.width)
3395 .max()
3396 .unwrap_or_default();
3397
3398 let mut current_position = gpui::Point {
3399 // Snap the right edge of the list to the right edge of the window if its horizontal bounds
3400 // overflow. Include space for the scrollbar.
3401 x: target_position
3402 .x
3403 .min((viewport_bounds.right() - max_width).max(Pixels::ZERO)),
3404 y: if y_flipped {
3405 bottom_y_when_flipped
3406 } else {
3407 target_position.y
3408 },
3409 };
3410
3411 let mut laid_out_popovers = popovers
3412 .into_iter()
3413 .map(|(popover_type, element, size)| {
3414 if y_flipped {
3415 current_position.y -= size.height;
3416 }
3417 let position = current_position;
3418 window.defer_draw(element, current_position, 1);
3419 if !y_flipped {
3420 current_position.y += size.height + MENU_GAP;
3421 } else {
3422 current_position.y -= MENU_GAP;
3423 }
3424 (popover_type, Bounds::new(position, size))
3425 })
3426 .collect::<Vec<_>>();
3427
3428 if y_flipped {
3429 laid_out_popovers.reverse();
3430 }
3431
3432 Some((laid_out_popovers, y_flipped))
3433 })
3434 }
3435
3436 #[allow(clippy::too_many_arguments)]
3437 fn layout_context_menu_aside(
3438 &self,
3439 y_flipped: bool,
3440 menu_bounds: Bounds<Pixels>,
3441 target_bounds: Bounds<Pixels>,
3442 max_target_bounds: Bounds<Pixels>,
3443 max_height: Pixels,
3444 must_place_above_or_below: bool,
3445 text_hitbox: &Hitbox,
3446 viewport_bounds: Bounds<Pixels>,
3447 window: &mut Window,
3448 cx: &mut App,
3449 ) {
3450 let available_within_viewport = target_bounds.space_within(&viewport_bounds);
3451 let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH
3452 && !must_place_above_or_below
3453 {
3454 let max_width = cmp::min(
3455 available_within_viewport.right - px(1.),
3456 MENU_ASIDE_MAX_WIDTH,
3457 );
3458 let Some(mut aside) = self.render_context_menu_aside(
3459 size(max_width, max_height - POPOVER_Y_PADDING),
3460 window,
3461 cx,
3462 ) else {
3463 return;
3464 };
3465 aside.layout_as_root(AvailableSpace::min_size(), window, cx);
3466 let right_position = point(target_bounds.right(), menu_bounds.origin.y);
3467 Some((aside, right_position))
3468 } else {
3469 let max_size = size(
3470 // TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
3471 // won't be needed here.
3472 cmp::min(
3473 cmp::max(menu_bounds.size.width - px(2.), MENU_ASIDE_MIN_WIDTH),
3474 viewport_bounds.right(),
3475 ),
3476 cmp::min(
3477 max_height,
3478 cmp::max(
3479 available_within_viewport.top,
3480 available_within_viewport.bottom,
3481 ),
3482 ) - POPOVER_Y_PADDING,
3483 );
3484 let Some(mut aside) = self.render_context_menu_aside(max_size, window, cx) else {
3485 return;
3486 };
3487 let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
3488
3489 let top_position = point(
3490 menu_bounds.origin.x,
3491 target_bounds.top() - actual_size.height,
3492 );
3493 let bottom_position = point(menu_bounds.origin.x, target_bounds.bottom());
3494
3495 let fit_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
3496 // Prefer to fit on the same side of the line as the menu, then on the other side of
3497 // the line.
3498 if !y_flipped && wanted.height < available.bottom {
3499 Some(bottom_position)
3500 } else if !y_flipped && wanted.height < available.top {
3501 Some(top_position)
3502 } else if y_flipped && wanted.height < available.top {
3503 Some(top_position)
3504 } else if y_flipped && wanted.height < available.bottom {
3505 Some(bottom_position)
3506 } else {
3507 None
3508 }
3509 };
3510
3511 // Prefer choosing a direction using max sizes rather than actual size for stability.
3512 let available_within_text = max_target_bounds.space_within(&text_hitbox.bounds);
3513 let wanted = size(MENU_ASIDE_MAX_WIDTH, max_height);
3514 let aside_position = fit_within(available_within_text, wanted)
3515 // Fallback: fit max size in window.
3516 .or_else(|| fit_within(max_target_bounds.space_within(&viewport_bounds), wanted))
3517 // Fallback: fit actual size in window.
3518 .or_else(|| fit_within(available_within_viewport, actual_size));
3519
3520 aside_position.map(|position| (aside, position))
3521 };
3522
3523 // Skip drawing if it doesn't fit anywhere.
3524 if let Some((aside, position)) = positioned_aside {
3525 window.defer_draw(aside, position, 2);
3526 }
3527 }
3528
3529 fn render_context_menu(
3530 &self,
3531 line_height: Pixels,
3532 height: Pixels,
3533 y_flipped: bool,
3534 window: &mut Window,
3535 cx: &mut App,
3536 ) -> Option<AnyElement> {
3537 let max_height_in_lines = ((height - POPOVER_Y_PADDING) / line_height).floor() as u32;
3538 self.editor.update(cx, |editor, cx| {
3539 editor.render_context_menu(&self.style, max_height_in_lines, y_flipped, window, cx)
3540 })
3541 }
3542
3543 fn render_context_menu_aside(
3544 &self,
3545 max_size: Size<Pixels>,
3546 window: &mut Window,
3547 cx: &mut App,
3548 ) -> Option<AnyElement> {
3549 if max_size.width < px(100.) || max_size.height < px(12.) {
3550 None
3551 } else {
3552 self.editor.update(cx, |editor, cx| {
3553 editor.render_context_menu_aside(max_size, window, cx)
3554 })
3555 }
3556 }
3557
3558 #[allow(clippy::too_many_arguments)]
3559 fn layout_edit_prediction_popover(
3560 &self,
3561 text_bounds: &Bounds<Pixels>,
3562 content_origin: gpui::Point<Pixels>,
3563 editor_snapshot: &EditorSnapshot,
3564 visible_row_range: Range<DisplayRow>,
3565 scroll_top: f32,
3566 scroll_bottom: f32,
3567 line_layouts: &[LineWithInvisibles],
3568 line_height: Pixels,
3569 scroll_pixel_position: gpui::Point<Pixels>,
3570 newest_selection_head: Option<DisplayPoint>,
3571 editor_width: Pixels,
3572 style: &EditorStyle,
3573 window: &mut Window,
3574 cx: &mut App,
3575 ) -> Option<AnyElement> {
3576 const PADDING_X: Pixels = Pixels(24.);
3577 const PADDING_Y: Pixels = Pixels(2.);
3578
3579 let editor = self.editor.read(cx);
3580 let active_inline_completion = editor.active_inline_completion.as_ref()?;
3581
3582 if editor.edit_prediction_visible_in_cursor_popover(true) {
3583 return None;
3584 }
3585
3586 // Adjust text origin for horizontal scrolling (in some cases here)
3587 let start_point = content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
3588
3589 // Clamp left offset after extreme scrollings
3590 let clamp_start = |point: gpui::Point<Pixels>| gpui::Point {
3591 x: point.x.max(content_origin.x),
3592 y: point.y,
3593 };
3594
3595 match &active_inline_completion.completion {
3596 InlineCompletion::Move { target, .. } => {
3597 let target_display_point = target.to_display_point(editor_snapshot);
3598
3599 if editor.edit_prediction_requires_modifier() {
3600 if !editor.edit_prediction_preview_is_active() {
3601 return None;
3602 }
3603
3604 if target_display_point.row() < visible_row_range.start {
3605 let mut element = editor
3606 .render_edit_prediction_line_popover(
3607 "Scroll",
3608 Some(IconName::ArrowUp),
3609 window,
3610 cx,
3611 )?
3612 .into_any();
3613
3614 element.layout_as_root(AvailableSpace::min_size(), window, cx);
3615
3616 let cursor = newest_selection_head?;
3617 let cursor_row_layout = line_layouts
3618 .get(cursor.row().minus(visible_row_range.start) as usize)?;
3619 let cursor_column = cursor.column() as usize;
3620
3621 let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
3622
3623 const PADDING_Y: Pixels = px(12.);
3624
3625 let origin = start_point + point(cursor_character_x, PADDING_Y);
3626
3627 element.prepaint_at(origin, window, cx);
3628 return Some(element);
3629 } else if target_display_point.row() >= visible_row_range.end {
3630 let mut element = editor
3631 .render_edit_prediction_line_popover(
3632 "Scroll",
3633 Some(IconName::ArrowDown),
3634 window,
3635 cx,
3636 )?
3637 .into_any();
3638
3639 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3640
3641 let cursor = newest_selection_head?;
3642 let cursor_row_layout = line_layouts
3643 .get(cursor.row().minus(visible_row_range.start) as usize)?;
3644 let cursor_column = cursor.column() as usize;
3645
3646 let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
3647 const PADDING_Y: Pixels = px(12.);
3648
3649 let origin = start_point
3650 + point(
3651 cursor_character_x,
3652 text_bounds.size.height - size.height - PADDING_Y,
3653 );
3654
3655 element.prepaint_at(origin, window, cx);
3656 return Some(element);
3657 } else {
3658 const POLE_WIDTH: Pixels = px(2.);
3659
3660 let mut element = v_flex()
3661 .items_end()
3662 .child(
3663 editor
3664 .render_edit_prediction_line_popover("Jump", None, window, cx)?
3665 .rounded_br(px(0.))
3666 .rounded_tr(px(0.))
3667 .border_r_2(),
3668 )
3669 .child(
3670 div()
3671 .w(POLE_WIDTH)
3672 .bg(Editor::edit_prediction_callout_popover_border_color(cx))
3673 .h(line_height),
3674 )
3675 .into_any();
3676
3677 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3678
3679 let line_layout =
3680 line_layouts
3681 .get(target_display_point.row().minus(visible_row_range.start)
3682 as usize)?;
3683 let target_column = target_display_point.column() as usize;
3684
3685 let target_x = line_layout.x_for_index(target_column);
3686 let target_y = (target_display_point.row().as_f32() * line_height)
3687 - scroll_pixel_position.y;
3688
3689 let origin = clamp_start(
3690 start_point + point(target_x, target_y)
3691 - point(size.width - POLE_WIDTH, size.height - line_height),
3692 );
3693
3694 element.prepaint_at(origin, window, cx);
3695
3696 return Some(element);
3697 }
3698 }
3699
3700 if target_display_point.row().as_f32() < scroll_top {
3701 let mut element = editor
3702 .render_edit_prediction_line_popover(
3703 "Jump to Edit",
3704 Some(IconName::ArrowUp),
3705 window,
3706 cx,
3707 )?
3708 .into_any();
3709
3710 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3711 let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
3712
3713 element.prepaint_at(text_bounds.origin + offset, window, cx);
3714 Some(element)
3715 } else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
3716 let mut element = editor
3717 .render_edit_prediction_line_popover(
3718 "Jump to Edit",
3719 Some(IconName::ArrowDown),
3720 window,
3721 cx,
3722 )?
3723 .into_any();
3724
3725 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3726 let offset = point(
3727 (text_bounds.size.width - size.width) / 2.,
3728 text_bounds.size.height - size.height - PADDING_Y,
3729 );
3730
3731 element.prepaint_at(text_bounds.origin + offset, window, cx);
3732 Some(element)
3733 } else {
3734 let mut element = editor
3735 .render_edit_prediction_line_popover("Jump to Edit", None, window, cx)?
3736 .into_any();
3737 let target_line_end = DisplayPoint::new(
3738 target_display_point.row(),
3739 editor_snapshot.line_len(target_display_point.row()),
3740 );
3741 let origin = self.editor.update(cx, |editor, _cx| {
3742 editor.display_to_pixel_point(target_line_end, editor_snapshot, window)
3743 })?;
3744
3745 element.prepaint_as_root(
3746 clamp_start(start_point + origin + point(PADDING_X, px(0.))),
3747 AvailableSpace::min_size(),
3748 window,
3749 cx,
3750 );
3751 Some(element)
3752 }
3753 }
3754 InlineCompletion::Edit {
3755 edits,
3756 edit_preview,
3757 display_mode,
3758 snapshot,
3759 } => {
3760 if self.editor.read(cx).has_visible_completions_menu() {
3761 return None;
3762 }
3763
3764 let edit_start = edits
3765 .first()
3766 .unwrap()
3767 .0
3768 .start
3769 .to_display_point(editor_snapshot);
3770 let edit_end = edits
3771 .last()
3772 .unwrap()
3773 .0
3774 .end
3775 .to_display_point(editor_snapshot);
3776
3777 let is_visible = visible_row_range.contains(&edit_start.row())
3778 || visible_row_range.contains(&edit_end.row());
3779 if !is_visible {
3780 return None;
3781 }
3782
3783 match display_mode {
3784 EditDisplayMode::TabAccept => {
3785 let range = &edits.first()?.0;
3786 let target_display_point = range.end.to_display_point(editor_snapshot);
3787
3788 let target_line_end = DisplayPoint::new(
3789 target_display_point.row(),
3790 editor_snapshot.line_len(target_display_point.row()),
3791 );
3792 let (mut element, origin) = self.editor.update(cx, |editor, cx| {
3793 Some((
3794 editor
3795 .render_edit_prediction_line_popover(
3796 "Accept", None, window, cx,
3797 )?
3798 .into_any(),
3799 editor.display_to_pixel_point(
3800 target_line_end,
3801 editor_snapshot,
3802 window,
3803 )?,
3804 ))
3805 })?;
3806
3807 element.prepaint_as_root(
3808 clamp_start(start_point + origin + point(PADDING_X, px(0.))),
3809 AvailableSpace::min_size(),
3810 window,
3811 cx,
3812 );
3813 return Some(element);
3814 }
3815 EditDisplayMode::Inline => return None,
3816 EditDisplayMode::DiffPopover => {}
3817 }
3818
3819 let highlighted_edits = crate::inline_completion_edit_text(
3820 &snapshot,
3821 edits,
3822 edit_preview.as_ref()?,
3823 false,
3824 cx,
3825 );
3826
3827 let styled_text = highlighted_edits.to_styled_text(&style.text);
3828 let line_count = highlighted_edits.text.lines().count();
3829
3830 const BORDER_WIDTH: Pixels = px(1.);
3831
3832 let mut element = h_flex()
3833 .items_start()
3834 .child(
3835 h_flex()
3836 .bg(cx.theme().colors().editor_background)
3837 .border(BORDER_WIDTH)
3838 .shadow_sm()
3839 .border_color(cx.theme().colors().border)
3840 .rounded_l_lg()
3841 .when(line_count > 1, |el| el.rounded_br_lg())
3842 .pr_1()
3843 .child(styled_text),
3844 )
3845 .child(
3846 h_flex()
3847 .h(line_height + BORDER_WIDTH * px(2.))
3848 .px_1p5()
3849 .gap_1()
3850 // Workaround: For some reason, there's a gap if we don't do this
3851 .ml(-BORDER_WIDTH)
3852 .shadow(smallvec![gpui::BoxShadow {
3853 color: gpui::black().opacity(0.05),
3854 offset: point(px(1.), px(1.)),
3855 blur_radius: px(2.),
3856 spread_radius: px(0.),
3857 }])
3858 .bg(Editor::edit_prediction_line_popover_bg_color(cx))
3859 .border(BORDER_WIDTH)
3860 .border_color(cx.theme().colors().border)
3861 .rounded_r_lg()
3862 .children(editor.render_edit_prediction_accept_keybind(window, cx)),
3863 )
3864 .into_any();
3865
3866 let longest_row =
3867 editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
3868 let longest_line_width = if visible_row_range.contains(&longest_row) {
3869 line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
3870 } else {
3871 layout_line(
3872 longest_row,
3873 editor_snapshot,
3874 style,
3875 editor_width,
3876 |_| false,
3877 window,
3878 cx,
3879 )
3880 .width
3881 };
3882
3883 let viewport_bounds = Bounds::new(Default::default(), window.viewport_size())
3884 .extend(Edges {
3885 right: -Self::SCROLLBAR_WIDTH,
3886 ..Default::default()
3887 });
3888
3889 let x_after_longest =
3890 text_bounds.origin.x + longest_line_width + PADDING_X - scroll_pixel_position.x;
3891
3892 let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3893
3894 // Fully visible if it can be displayed within the window (allow overlapping other
3895 // panes). However, this is only allowed if the popover starts within text_bounds.
3896 let can_position_to_the_right = x_after_longest < text_bounds.right()
3897 && x_after_longest + element_bounds.width < viewport_bounds.right();
3898
3899 let mut origin = if can_position_to_the_right {
3900 point(
3901 x_after_longest,
3902 text_bounds.origin.y + edit_start.row().as_f32() * line_height
3903 - scroll_pixel_position.y,
3904 )
3905 } else {
3906 let cursor_row = newest_selection_head.map(|head| head.row());
3907 let above_edit = edit_start
3908 .row()
3909 .0
3910 .checked_sub(line_count as u32)
3911 .map(DisplayRow);
3912 let below_edit = Some(edit_end.row() + 1);
3913 let above_cursor = cursor_row
3914 .and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
3915 let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
3916
3917 // Place the edit popover adjacent to the edit if there is a location
3918 // available that is onscreen and does not obscure the cursor. Otherwise,
3919 // place it adjacent to the cursor.
3920 let row_target = [above_edit, below_edit, above_cursor, below_cursor]
3921 .into_iter()
3922 .flatten()
3923 .find(|&start_row| {
3924 let end_row = start_row + line_count as u32;
3925 visible_row_range.contains(&start_row)
3926 && visible_row_range.contains(&end_row)
3927 && cursor_row.map_or(true, |cursor_row| {
3928 !((start_row..end_row).contains(&cursor_row))
3929 })
3930 })?;
3931
3932 content_origin
3933 + point(
3934 -scroll_pixel_position.x,
3935 row_target.as_f32() * line_height - scroll_pixel_position.y,
3936 )
3937 };
3938
3939 origin.x -= BORDER_WIDTH;
3940
3941 window.defer_draw(element, origin, 1);
3942
3943 // Do not return an element, since it will already be drawn due to defer_draw.
3944 None
3945 }
3946 }
3947 }
3948
3949 fn layout_mouse_context_menu(
3950 &self,
3951 editor_snapshot: &EditorSnapshot,
3952 visible_range: Range<DisplayRow>,
3953 content_origin: gpui::Point<Pixels>,
3954 window: &mut Window,
3955 cx: &mut App,
3956 ) -> Option<AnyElement> {
3957 let position = self.editor.update(cx, |editor, _cx| {
3958 let visible_start_point = editor.display_to_pixel_point(
3959 DisplayPoint::new(visible_range.start, 0),
3960 editor_snapshot,
3961 window,
3962 )?;
3963 let visible_end_point = editor.display_to_pixel_point(
3964 DisplayPoint::new(visible_range.end, 0),
3965 editor_snapshot,
3966 window,
3967 )?;
3968
3969 let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
3970 let (source_display_point, position) = match mouse_context_menu.position {
3971 MenuPosition::PinnedToScreen(point) => (None, point),
3972 MenuPosition::PinnedToEditor { source, offset } => {
3973 let source_display_point = source.to_display_point(editor_snapshot);
3974 let source_point = editor.to_pixel_point(source, editor_snapshot, window)?;
3975 let position = content_origin + source_point + offset;
3976 (Some(source_display_point), position)
3977 }
3978 };
3979
3980 let source_included = source_display_point.map_or(true, |source_display_point| {
3981 visible_range
3982 .to_inclusive()
3983 .contains(&source_display_point.row())
3984 });
3985 let position_included =
3986 visible_start_point.y <= position.y && position.y <= visible_end_point.y;
3987 if !source_included && !position_included {
3988 None
3989 } else {
3990 Some(position)
3991 }
3992 })?;
3993
3994 let text_style = TextStyleRefinement {
3995 line_height: Some(DefiniteLength::Fraction(
3996 BufferLineHeight::Comfortable.value(),
3997 )),
3998 ..Default::default()
3999 };
4000 window.with_text_style(Some(text_style), |window| {
4001 let mut element = self.editor.update(cx, |editor, _| {
4002 let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
4003 let context_menu = mouse_context_menu.context_menu.clone();
4004
4005 Some(
4006 deferred(
4007 anchored()
4008 .position(position)
4009 .child(context_menu)
4010 .anchor(Corner::TopLeft)
4011 .snap_to_window_with_margin(px(8.)),
4012 )
4013 .with_priority(1)
4014 .into_any(),
4015 )
4016 })?;
4017
4018 element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx);
4019 Some(element)
4020 })
4021 }
4022
4023 #[allow(clippy::too_many_arguments)]
4024 fn layout_hover_popovers(
4025 &self,
4026 snapshot: &EditorSnapshot,
4027 hitbox: &Hitbox,
4028 text_hitbox: &Hitbox,
4029 visible_display_row_range: Range<DisplayRow>,
4030 content_origin: gpui::Point<Pixels>,
4031 scroll_pixel_position: gpui::Point<Pixels>,
4032 line_layouts: &[LineWithInvisibles],
4033 line_height: Pixels,
4034 em_width: Pixels,
4035 window: &mut Window,
4036 cx: &mut App,
4037 ) {
4038 struct MeasuredHoverPopover {
4039 element: AnyElement,
4040 size: Size<Pixels>,
4041 horizontal_offset: Pixels,
4042 }
4043
4044 let max_size = size(
4045 (120. * em_width) // Default size
4046 .min(hitbox.size.width / 2.) // Shrink to half of the editor width
4047 .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
4048 (16. * line_height) // Default size
4049 .min(hitbox.size.height / 2.) // Shrink to half of the editor height
4050 .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
4051 );
4052
4053 let hover_popovers = self.editor.update(cx, |editor, cx| {
4054 editor
4055 .hover_state
4056 .render(snapshot, visible_display_row_range.clone(), max_size, cx)
4057 });
4058 let Some((position, hover_popovers)) = hover_popovers else {
4059 return;
4060 };
4061
4062 // This is safe because we check on layout whether the required row is available
4063 let hovered_row_layout =
4064 &line_layouts[position.row().minus(visible_display_row_range.start) as usize];
4065
4066 // Compute Hovered Point
4067 let x =
4068 hovered_row_layout.x_for_index(position.column() as usize) - scroll_pixel_position.x;
4069 let y = position.row().as_f32() * line_height - scroll_pixel_position.y;
4070 let hovered_point = content_origin + point(x, y);
4071
4072 let mut overall_height = Pixels::ZERO;
4073 let mut measured_hover_popovers = Vec::new();
4074 for mut hover_popover in hover_popovers {
4075 let size = hover_popover.layout_as_root(AvailableSpace::min_size(), window, cx);
4076 let horizontal_offset =
4077 (text_hitbox.top_right().x - (hovered_point.x + size.width)).min(Pixels::ZERO);
4078
4079 overall_height += HOVER_POPOVER_GAP + size.height;
4080
4081 measured_hover_popovers.push(MeasuredHoverPopover {
4082 element: hover_popover,
4083 size,
4084 horizontal_offset,
4085 });
4086 }
4087 overall_height += HOVER_POPOVER_GAP;
4088
4089 fn draw_occluder(
4090 width: Pixels,
4091 origin: gpui::Point<Pixels>,
4092 window: &mut Window,
4093 cx: &mut App,
4094 ) {
4095 let mut occlusion = div()
4096 .size_full()
4097 .occlude()
4098 .on_mouse_move(|_, _, cx| cx.stop_propagation())
4099 .into_any_element();
4100 occlusion.layout_as_root(size(width, HOVER_POPOVER_GAP).into(), window, cx);
4101 window.defer_draw(occlusion, origin, 2);
4102 }
4103
4104 if hovered_point.y > overall_height {
4105 // There is enough space above. Render popovers above the hovered point
4106 let mut current_y = hovered_point.y;
4107 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
4108 let size = popover.size;
4109 let popover_origin = point(
4110 hovered_point.x + popover.horizontal_offset,
4111 current_y - size.height,
4112 );
4113
4114 window.defer_draw(popover.element, popover_origin, 2);
4115 if position != itertools::Position::Last {
4116 let origin = point(popover_origin.x, popover_origin.y - HOVER_POPOVER_GAP);
4117 draw_occluder(size.width, origin, window, cx);
4118 }
4119
4120 current_y = popover_origin.y - HOVER_POPOVER_GAP;
4121 }
4122 } else {
4123 // There is not enough space above. Render popovers below the hovered point
4124 let mut current_y = hovered_point.y + line_height;
4125 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
4126 let size = popover.size;
4127 let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
4128
4129 window.defer_draw(popover.element, popover_origin, 2);
4130 if position != itertools::Position::Last {
4131 let origin = point(popover_origin.x, popover_origin.y + size.height);
4132 draw_occluder(size.width, origin, window, cx);
4133 }
4134
4135 current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
4136 }
4137 }
4138 }
4139
4140 #[allow(clippy::too_many_arguments)]
4141 fn layout_diff_hunk_controls(
4142 &self,
4143 row_range: Range<DisplayRow>,
4144 row_infos: &[RowInfo],
4145 text_hitbox: &Hitbox,
4146 position_map: &PositionMap,
4147 newest_cursor_position: Option<DisplayPoint>,
4148 line_height: Pixels,
4149 scroll_pixel_position: gpui::Point<Pixels>,
4150 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
4151 editor: Entity<Editor>,
4152 window: &mut Window,
4153 cx: &mut App,
4154 ) -> Vec<AnyElement> {
4155 let point_for_position = position_map.point_for_position(window.mouse_position());
4156
4157 let mut controls = vec![];
4158
4159 let active_positions = [
4160 Some(point_for_position.previous_valid),
4161 newest_cursor_position,
4162 ];
4163
4164 for (hunk, _) in display_hunks {
4165 if let DisplayDiffHunk::Unfolded {
4166 display_row_range,
4167 multi_buffer_range,
4168 status,
4169 ..
4170 } = &hunk
4171 {
4172 if display_row_range.start < row_range.start
4173 || display_row_range.start >= row_range.end
4174 {
4175 continue;
4176 }
4177 let row_ix = (display_row_range.start - row_range.start).0 as usize;
4178 if row_infos[row_ix].diff_status.is_none() {
4179 continue;
4180 }
4181 if row_infos[row_ix]
4182 .diff_status
4183 .is_some_and(|status| status.is_added())
4184 && !status.is_added()
4185 {
4186 continue;
4187 }
4188 if active_positions
4189 .iter()
4190 .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row())))
4191 {
4192 let y = display_row_range.start.as_f32() * line_height
4193 + text_hitbox.bounds.top()
4194 - scroll_pixel_position.y;
4195 let x = text_hitbox.bounds.right() - px(100.);
4196
4197 let mut element = diff_hunk_controls(
4198 display_row_range.start.0,
4199 multi_buffer_range.clone(),
4200 line_height,
4201 &editor,
4202 cx,
4203 );
4204 element.prepaint_as_root(
4205 gpui::Point::new(x, y),
4206 size(px(100.0), line_height).into(),
4207 window,
4208 cx,
4209 );
4210 controls.push(element);
4211 }
4212 }
4213 }
4214
4215 controls
4216 }
4217
4218 #[allow(clippy::too_many_arguments)]
4219 fn layout_signature_help(
4220 &self,
4221 hitbox: &Hitbox,
4222 content_origin: gpui::Point<Pixels>,
4223 scroll_pixel_position: gpui::Point<Pixels>,
4224 newest_selection_head: Option<DisplayPoint>,
4225 start_row: DisplayRow,
4226 line_layouts: &[LineWithInvisibles],
4227 line_height: Pixels,
4228 em_width: Pixels,
4229 window: &mut Window,
4230 cx: &mut App,
4231 ) {
4232 if !self.editor.focus_handle(cx).is_focused(window) {
4233 return;
4234 }
4235 let Some(newest_selection_head) = newest_selection_head else {
4236 return;
4237 };
4238 let selection_row = newest_selection_head.row();
4239 if selection_row < start_row {
4240 return;
4241 }
4242 let Some(cursor_row_layout) = line_layouts.get(selection_row.minus(start_row) as usize)
4243 else {
4244 return;
4245 };
4246
4247 let start_x = cursor_row_layout.x_for_index(newest_selection_head.column() as usize)
4248 - scroll_pixel_position.x
4249 + content_origin.x;
4250 let start_y =
4251 selection_row.as_f32() * line_height + content_origin.y - scroll_pixel_position.y;
4252
4253 let max_size = size(
4254 (120. * em_width) // Default size
4255 .min(hitbox.size.width / 2.) // Shrink to half of the editor width
4256 .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
4257 (16. * line_height) // Default size
4258 .min(hitbox.size.height / 2.) // Shrink to half of the editor height
4259 .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
4260 );
4261
4262 let maybe_element = self.editor.update(cx, |editor, cx| {
4263 if let Some(popover) = editor.signature_help_state.popover_mut() {
4264 let element = popover.render(max_size, cx);
4265 Some(element)
4266 } else {
4267 None
4268 }
4269 });
4270 if let Some(mut element) = maybe_element {
4271 let window_size = window.viewport_size();
4272 let size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
4273 let mut point = point(start_x, start_y - size.height);
4274
4275 // Adjusting to ensure the popover does not overflow in the X-axis direction.
4276 if point.x + size.width >= window_size.width {
4277 point.x = window_size.width - size.width;
4278 }
4279
4280 window.defer_draw(element, point, 1)
4281 }
4282 }
4283
4284 fn paint_background(&self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
4285 window.paint_layer(layout.hitbox.bounds, |window| {
4286 let scroll_top = layout.position_map.snapshot.scroll_position().y;
4287 let gutter_bg = cx.theme().colors().editor_gutter_background;
4288 window.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg));
4289 window.paint_quad(fill(
4290 layout.position_map.text_hitbox.bounds,
4291 self.style.background,
4292 ));
4293
4294 if let EditorMode::Full = layout.mode {
4295 let mut active_rows = layout.active_rows.iter().peekable();
4296 while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
4297 let mut end_row = start_row.0;
4298 while active_rows
4299 .peek()
4300 .map_or(false, |(active_row, has_selection)| {
4301 active_row.0 == end_row + 1
4302 && *has_selection == contains_non_empty_selection
4303 })
4304 {
4305 active_rows.next().unwrap();
4306 end_row += 1;
4307 }
4308
4309 if !contains_non_empty_selection {
4310 let highlight_h_range =
4311 match layout.position_map.snapshot.current_line_highlight {
4312 CurrentLineHighlight::Gutter => Some(Range {
4313 start: layout.hitbox.left(),
4314 end: layout.gutter_hitbox.right(),
4315 }),
4316 CurrentLineHighlight::Line => Some(Range {
4317 start: layout.position_map.text_hitbox.bounds.left(),
4318 end: layout.position_map.text_hitbox.bounds.right(),
4319 }),
4320 CurrentLineHighlight::All => Some(Range {
4321 start: layout.hitbox.left(),
4322 end: layout.hitbox.right(),
4323 }),
4324 CurrentLineHighlight::None => None,
4325 };
4326 if let Some(range) = highlight_h_range {
4327 let active_line_bg = cx.theme().colors().editor_active_line_background;
4328 let bounds = Bounds {
4329 origin: point(
4330 range.start,
4331 layout.hitbox.origin.y
4332 + (start_row.as_f32() - scroll_top)
4333 * layout.position_map.line_height,
4334 ),
4335 size: size(
4336 range.end - range.start,
4337 layout.position_map.line_height
4338 * (end_row - start_row.0 + 1) as f32,
4339 ),
4340 };
4341 window.paint_quad(fill(bounds, active_line_bg));
4342 }
4343 }
4344 }
4345
4346 let mut paint_highlight =
4347 |highlight_row_start: DisplayRow, highlight_row_end: DisplayRow, color| {
4348 let origin = point(
4349 layout.hitbox.origin.x,
4350 layout.hitbox.origin.y
4351 + (highlight_row_start.as_f32() - scroll_top)
4352 * layout.position_map.line_height,
4353 );
4354 let size = size(
4355 layout.hitbox.size.width,
4356 layout.position_map.line_height
4357 * highlight_row_end.next_row().minus(highlight_row_start) as f32,
4358 );
4359 window.paint_quad(fill(Bounds { origin, size }, color));
4360 };
4361
4362 let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
4363 for (&new_row, &new_background) in &layout.highlighted_rows {
4364 match &mut current_paint {
4365 Some((current_background, current_range)) => {
4366 let current_background = *current_background;
4367 let new_range_started = current_background != new_background
4368 || current_range.end.next_row() != new_row;
4369 if new_range_started {
4370 paint_highlight(
4371 current_range.start,
4372 current_range.end,
4373 current_background,
4374 );
4375 current_paint = Some((new_background, new_row..new_row));
4376 continue;
4377 } else {
4378 current_range.end = current_range.end.next_row();
4379 }
4380 }
4381 None => current_paint = Some((new_background, new_row..new_row)),
4382 };
4383 }
4384 if let Some((color, range)) = current_paint {
4385 paint_highlight(range.start, range.end, color);
4386 }
4387
4388 let scroll_left =
4389 layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width;
4390
4391 for (wrap_position, active) in layout.wrap_guides.iter() {
4392 let x = (layout.position_map.text_hitbox.origin.x
4393 + *wrap_position
4394 + layout.position_map.em_width / 2.)
4395 - scroll_left;
4396
4397 let show_scrollbars = {
4398 let (scrollbar_x, scrollbar_y) = &layout.scrollbars_layout.as_xy();
4399
4400 scrollbar_x.as_ref().map_or(false, |sx| sx.visible)
4401 || scrollbar_y.as_ref().map_or(false, |sy| sy.visible)
4402 };
4403
4404 if x < layout.position_map.text_hitbox.origin.x
4405 || (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds))
4406 {
4407 continue;
4408 }
4409
4410 let color = if *active {
4411 cx.theme().colors().editor_active_wrap_guide
4412 } else {
4413 cx.theme().colors().editor_wrap_guide
4414 };
4415 window.paint_quad(fill(
4416 Bounds {
4417 origin: point(x, layout.position_map.text_hitbox.origin.y),
4418 size: size(px(1.), layout.position_map.text_hitbox.size.height),
4419 },
4420 color,
4421 ));
4422 }
4423 }
4424 })
4425 }
4426
4427 fn paint_indent_guides(
4428 &mut self,
4429 layout: &mut EditorLayout,
4430 window: &mut Window,
4431 cx: &mut App,
4432 ) {
4433 let Some(indent_guides) = &layout.indent_guides else {
4434 return;
4435 };
4436
4437 let faded_color = |color: Hsla, alpha: f32| {
4438 let mut faded = color;
4439 faded.a = alpha;
4440 faded
4441 };
4442
4443 for indent_guide in indent_guides {
4444 let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
4445 let settings = indent_guide.settings;
4446
4447 // TODO fixed for now, expose them through themes later
4448 const INDENT_AWARE_ALPHA: f32 = 0.2;
4449 const INDENT_AWARE_ACTIVE_ALPHA: f32 = 0.4;
4450 const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
4451 const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
4452
4453 let line_color = match (settings.coloring, indent_guide.active) {
4454 (IndentGuideColoring::Disabled, _) => None,
4455 (IndentGuideColoring::Fixed, false) => {
4456 Some(cx.theme().colors().editor_indent_guide)
4457 }
4458 (IndentGuideColoring::Fixed, true) => {
4459 Some(cx.theme().colors().editor_indent_guide_active)
4460 }
4461 (IndentGuideColoring::IndentAware, false) => {
4462 Some(faded_color(indent_accent_colors, INDENT_AWARE_ALPHA))
4463 }
4464 (IndentGuideColoring::IndentAware, true) => {
4465 Some(faded_color(indent_accent_colors, INDENT_AWARE_ACTIVE_ALPHA))
4466 }
4467 };
4468
4469 let background_color = match (settings.background_coloring, indent_guide.active) {
4470 (IndentGuideBackgroundColoring::Disabled, _) => None,
4471 (IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
4472 indent_accent_colors,
4473 INDENT_AWARE_BACKGROUND_ALPHA,
4474 )),
4475 (IndentGuideBackgroundColoring::IndentAware, true) => Some(faded_color(
4476 indent_accent_colors,
4477 INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA,
4478 )),
4479 };
4480
4481 let requested_line_width = if indent_guide.active {
4482 settings.active_line_width
4483 } else {
4484 settings.line_width
4485 }
4486 .clamp(1, 10);
4487 let mut line_indicator_width = 0.;
4488 if let Some(color) = line_color {
4489 window.paint_quad(fill(
4490 Bounds {
4491 origin: indent_guide.origin,
4492 size: size(px(requested_line_width as f32), indent_guide.length),
4493 },
4494 color,
4495 ));
4496 line_indicator_width = requested_line_width as f32;
4497 }
4498
4499 if let Some(color) = background_color {
4500 let width = indent_guide.single_indent_width - px(line_indicator_width);
4501 window.paint_quad(fill(
4502 Bounds {
4503 origin: point(
4504 indent_guide.origin.x + px(line_indicator_width),
4505 indent_guide.origin.y,
4506 ),
4507 size: size(width, indent_guide.length),
4508 },
4509 color,
4510 ));
4511 }
4512 }
4513 }
4514
4515 fn paint_line_numbers(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4516 let is_singleton = self.editor.read(cx).is_singleton(cx);
4517
4518 let line_height = layout.position_map.line_height;
4519 window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
4520
4521 for LineNumberLayout {
4522 shaped_line,
4523 hitbox,
4524 display_row,
4525 } in layout.line_numbers.values()
4526 {
4527 let Some(hitbox) = hitbox else {
4528 continue;
4529 };
4530
4531 let is_active = layout.active_rows.contains_key(&display_row);
4532
4533 let color = if is_active {
4534 cx.theme().colors().editor_active_line_number
4535 } else if !is_singleton && hitbox.is_hovered(window) {
4536 cx.theme().colors().editor_hover_line_number
4537 } else {
4538 cx.theme().colors().editor_line_number
4539 };
4540
4541 let Some(line) = self
4542 .shape_line_number(shaped_line.text.clone(), color, window)
4543 .log_err()
4544 else {
4545 continue;
4546 };
4547 let Some(()) = line.paint(hitbox.origin, line_height, window, cx).log_err() else {
4548 continue;
4549 };
4550 // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
4551 // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
4552 if is_singleton {
4553 window.set_cursor_style(CursorStyle::IBeam, &hitbox);
4554 } else {
4555 window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
4556 }
4557 }
4558 }
4559
4560 fn paint_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4561 if layout.display_hunks.is_empty() {
4562 return;
4563 }
4564
4565 let line_height = layout.position_map.line_height;
4566 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
4567 for (hunk, hitbox) in &layout.display_hunks {
4568 let hunk_to_paint = match hunk {
4569 DisplayDiffHunk::Folded { .. } => {
4570 let hunk_bounds = Self::diff_hunk_bounds(
4571 &layout.position_map.snapshot,
4572 line_height,
4573 layout.gutter_hitbox.bounds,
4574 hunk,
4575 );
4576 Some((
4577 hunk_bounds,
4578 cx.theme().colors().version_control_modified,
4579 Corners::all(px(0.)),
4580 DiffHunkSecondaryStatus::None,
4581 ))
4582 }
4583 DisplayDiffHunk::Unfolded {
4584 status,
4585 display_row_range,
4586 ..
4587 } => hitbox.as_ref().map(|hunk_hitbox| match status.kind {
4588 DiffHunkStatusKind::Added => (
4589 hunk_hitbox.bounds,
4590 cx.theme().colors().version_control_added,
4591 Corners::all(px(0.)),
4592 status.secondary,
4593 ),
4594 DiffHunkStatusKind::Modified => (
4595 hunk_hitbox.bounds,
4596 cx.theme().colors().version_control_modified,
4597 Corners::all(px(0.)),
4598 status.secondary,
4599 ),
4600 DiffHunkStatusKind::Deleted if !display_row_range.is_empty() => (
4601 hunk_hitbox.bounds,
4602 cx.theme().colors().version_control_deleted,
4603 Corners::all(px(0.)),
4604 status.secondary,
4605 ),
4606 DiffHunkStatusKind::Deleted => (
4607 Bounds::new(
4608 point(
4609 hunk_hitbox.origin.x - hunk_hitbox.size.width,
4610 hunk_hitbox.origin.y,
4611 ),
4612 size(hunk_hitbox.size.width * px(2.), hunk_hitbox.size.height),
4613 ),
4614 cx.theme().colors().version_control_deleted,
4615 Corners::all(1. * line_height),
4616 status.secondary,
4617 ),
4618 }),
4619 };
4620
4621 if let Some((hunk_bounds, background_color, corner_radii, secondary_status)) =
4622 hunk_to_paint
4623 {
4624 let background_color = if secondary_status != DiffHunkSecondaryStatus::None {
4625 background_color.opacity(0.3)
4626 } else {
4627 background_color.opacity(1.0)
4628 };
4629 window.paint_quad(quad(
4630 hunk_bounds,
4631 corner_radii,
4632 background_color,
4633 Edges::default(),
4634 transparent_black(),
4635 ));
4636 }
4637 }
4638 });
4639 }
4640
4641 fn diff_hunk_bounds(
4642 snapshot: &EditorSnapshot,
4643 line_height: Pixels,
4644 gutter_bounds: Bounds<Pixels>,
4645 hunk: &DisplayDiffHunk,
4646 ) -> Bounds<Pixels> {
4647 let scroll_position = snapshot.scroll_position();
4648 let scroll_top = scroll_position.y * line_height;
4649 let gutter_strip_width = (0.275 * line_height).floor();
4650
4651 match hunk {
4652 DisplayDiffHunk::Folded { display_row, .. } => {
4653 let start_y = display_row.as_f32() * line_height - scroll_top;
4654 let end_y = start_y + line_height;
4655 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
4656 let highlight_size = size(gutter_strip_width, end_y - start_y);
4657 Bounds::new(highlight_origin, highlight_size)
4658 }
4659 DisplayDiffHunk::Unfolded {
4660 display_row_range,
4661 status,
4662 ..
4663 } => {
4664 if status.is_deleted() && display_row_range.is_empty() {
4665 let row = display_row_range.start;
4666
4667 let offset = line_height / 2.;
4668 let start_y = row.as_f32() * line_height - offset - scroll_top;
4669 let end_y = start_y + line_height;
4670
4671 let width = (0.35 * line_height).floor();
4672 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
4673 let highlight_size = size(width, end_y - start_y);
4674 Bounds::new(highlight_origin, highlight_size)
4675 } else {
4676 let start_row = display_row_range.start;
4677 let end_row = display_row_range.end;
4678 // If we're in a multibuffer, row range span might include an
4679 // excerpt header, so if we were to draw the marker straight away,
4680 // the hunk might include the rows of that header.
4681 // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
4682 // Instead, we simply check whether the range we're dealing with includes
4683 // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header.
4684 let end_row_in_current_excerpt = snapshot
4685 .blocks_in_range(start_row..end_row)
4686 .find_map(|(start_row, block)| {
4687 if matches!(block, Block::ExcerptBoundary { .. }) {
4688 Some(start_row)
4689 } else {
4690 None
4691 }
4692 })
4693 .unwrap_or(end_row);
4694
4695 let start_y = start_row.as_f32() * line_height - scroll_top;
4696 let end_y = end_row_in_current_excerpt.as_f32() * line_height - scroll_top;
4697
4698 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
4699 let highlight_size = size(gutter_strip_width, end_y - start_y);
4700 Bounds::new(highlight_origin, highlight_size)
4701 }
4702 }
4703 }
4704 }
4705
4706 fn paint_gutter_indicators(
4707 &self,
4708 layout: &mut EditorLayout,
4709 window: &mut Window,
4710 cx: &mut App,
4711 ) {
4712 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
4713 window.with_element_namespace("crease_toggles", |window| {
4714 for crease_toggle in layout.crease_toggles.iter_mut().flatten() {
4715 crease_toggle.paint(window, cx);
4716 }
4717 });
4718
4719 for test_indicator in layout.test_indicators.iter_mut() {
4720 test_indicator.paint(window, cx);
4721 }
4722
4723 if let Some(indicator) = layout.code_actions_indicator.as_mut() {
4724 indicator.paint(window, cx);
4725 }
4726 });
4727 }
4728
4729 fn paint_gutter_highlights(
4730 &self,
4731 layout: &mut EditorLayout,
4732 window: &mut Window,
4733 cx: &mut App,
4734 ) {
4735 for (_, hunk_hitbox) in &layout.display_hunks {
4736 if let Some(hunk_hitbox) = hunk_hitbox {
4737 if !self
4738 .editor
4739 .read(cx)
4740 .buffer()
4741 .read(cx)
4742 .all_diff_hunks_expanded()
4743 {
4744 window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
4745 }
4746 }
4747 }
4748
4749 let show_git_gutter = layout
4750 .position_map
4751 .snapshot
4752 .show_git_diff_gutter
4753 .unwrap_or_else(|| {
4754 matches!(
4755 ProjectSettings::get_global(cx).git.git_gutter,
4756 Some(GitGutterSetting::TrackedFiles)
4757 )
4758 });
4759 if show_git_gutter {
4760 Self::paint_diff_hunks(layout, window, cx)
4761 }
4762
4763 let highlight_width = 0.275 * layout.position_map.line_height;
4764 let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
4765 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
4766 for (range, color) in &layout.highlighted_gutter_ranges {
4767 let start_row = if range.start.row() < layout.visible_display_row_range.start {
4768 layout.visible_display_row_range.start - DisplayRow(1)
4769 } else {
4770 range.start.row()
4771 };
4772 let end_row = if range.end.row() > layout.visible_display_row_range.end {
4773 layout.visible_display_row_range.end + DisplayRow(1)
4774 } else {
4775 range.end.row()
4776 };
4777
4778 let start_y = layout.gutter_hitbox.top()
4779 + start_row.0 as f32 * layout.position_map.line_height
4780 - layout.position_map.scroll_pixel_position.y;
4781 let end_y = layout.gutter_hitbox.top()
4782 + (end_row.0 + 1) as f32 * layout.position_map.line_height
4783 - layout.position_map.scroll_pixel_position.y;
4784 let bounds = Bounds::from_corners(
4785 point(layout.gutter_hitbox.left(), start_y),
4786 point(layout.gutter_hitbox.left() + highlight_width, end_y),
4787 );
4788 window.paint_quad(fill(bounds, *color).corner_radii(highlight_corner_radii));
4789 }
4790 });
4791 }
4792
4793 fn paint_blamed_display_rows(
4794 &self,
4795 layout: &mut EditorLayout,
4796 window: &mut Window,
4797 cx: &mut App,
4798 ) {
4799 let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
4800 return;
4801 };
4802
4803 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
4804 for mut blame_element in blamed_display_rows.into_iter() {
4805 blame_element.paint(window, cx);
4806 }
4807 })
4808 }
4809
4810 fn paint_text(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4811 window.with_content_mask(
4812 Some(ContentMask {
4813 bounds: layout.position_map.text_hitbox.bounds,
4814 }),
4815 |window| {
4816 let cursor_style = if self
4817 .editor
4818 .read(cx)
4819 .hovered_link_state
4820 .as_ref()
4821 .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
4822 {
4823 CursorStyle::PointingHand
4824 } else {
4825 CursorStyle::IBeam
4826 };
4827 window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
4828
4829 let invisible_display_ranges = self.paint_highlights(layout, window);
4830 self.paint_lines(&invisible_display_ranges, layout, window, cx);
4831 self.paint_redactions(layout, window);
4832 self.paint_cursors(layout, window, cx);
4833 self.paint_inline_blame(layout, window, cx);
4834 self.paint_diff_hunk_controls(layout, window, cx);
4835 window.with_element_namespace("crease_trailers", |window| {
4836 for trailer in layout.crease_trailers.iter_mut().flatten() {
4837 trailer.element.paint(window, cx);
4838 }
4839 });
4840 },
4841 )
4842 }
4843
4844 fn paint_highlights(
4845 &mut self,
4846 layout: &mut EditorLayout,
4847 window: &mut Window,
4848 ) -> SmallVec<[Range<DisplayPoint>; 32]> {
4849 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
4850 let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
4851 let line_end_overshoot = 0.15 * layout.position_map.line_height;
4852 for (range, color) in &layout.highlighted_ranges {
4853 self.paint_highlighted_range(
4854 range.clone(),
4855 *color,
4856 Pixels::ZERO,
4857 line_end_overshoot,
4858 layout,
4859 window,
4860 );
4861 }
4862
4863 let corner_radius = 0.15 * layout.position_map.line_height;
4864
4865 for (player_color, selections) in &layout.selections {
4866 for selection in selections.iter() {
4867 self.paint_highlighted_range(
4868 selection.range.clone(),
4869 player_color.selection,
4870 corner_radius,
4871 corner_radius * 2.,
4872 layout,
4873 window,
4874 );
4875
4876 if selection.is_local && !selection.range.is_empty() {
4877 invisible_display_ranges.push(selection.range.clone());
4878 }
4879 }
4880 }
4881 invisible_display_ranges
4882 })
4883 }
4884
4885 fn paint_lines(
4886 &mut self,
4887 invisible_display_ranges: &[Range<DisplayPoint>],
4888 layout: &mut EditorLayout,
4889 window: &mut Window,
4890 cx: &mut App,
4891 ) {
4892 let whitespace_setting = self
4893 .editor
4894 .read(cx)
4895 .buffer
4896 .read(cx)
4897 .settings_at(0, cx)
4898 .show_whitespaces;
4899
4900 for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
4901 let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
4902 line_with_invisibles.draw(
4903 layout,
4904 row,
4905 layout.content_origin,
4906 whitespace_setting,
4907 invisible_display_ranges,
4908 window,
4909 cx,
4910 )
4911 }
4912
4913 for line_element in &mut layout.line_elements {
4914 line_element.paint(window, cx);
4915 }
4916 }
4917
4918 fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
4919 if layout.redacted_ranges.is_empty() {
4920 return;
4921 }
4922
4923 let line_end_overshoot = layout.line_end_overshoot();
4924
4925 // A softer than perfect black
4926 let redaction_color = gpui::rgb(0x0e1111);
4927
4928 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
4929 for range in layout.redacted_ranges.iter() {
4930 self.paint_highlighted_range(
4931 range.clone(),
4932 redaction_color.into(),
4933 Pixels::ZERO,
4934 line_end_overshoot,
4935 layout,
4936 window,
4937 );
4938 }
4939 });
4940 }
4941
4942 fn paint_cursors(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4943 for cursor in &mut layout.visible_cursors {
4944 cursor.paint(layout.content_origin, window, cx);
4945 }
4946 }
4947
4948 fn paint_scrollbars(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4949 let (scrollbar_x, scrollbar_y) = layout.scrollbars_layout.as_xy();
4950
4951 if let Some(scrollbar_layout) = scrollbar_x {
4952 let hitbox = scrollbar_layout.hitbox.clone();
4953 let text_unit_size = scrollbar_layout.text_unit_size;
4954 let visible_range = scrollbar_layout.visible_range.clone();
4955 let thumb_bounds = scrollbar_layout.thumb_bounds();
4956
4957 if scrollbar_layout.visible {
4958 window.paint_layer(hitbox.bounds, |window| {
4959 window.paint_quad(quad(
4960 hitbox.bounds,
4961 Corners::default(),
4962 cx.theme().colors().scrollbar_track_background,
4963 Edges {
4964 top: Pixels::ZERO,
4965 right: Pixels::ZERO,
4966 bottom: Pixels::ZERO,
4967 left: Pixels::ZERO,
4968 },
4969 cx.theme().colors().scrollbar_track_border,
4970 ));
4971
4972 window.paint_quad(quad(
4973 thumb_bounds,
4974 Corners::default(),
4975 cx.theme().colors().scrollbar_thumb_background,
4976 Edges {
4977 top: Pixels::ZERO,
4978 right: Pixels::ZERO,
4979 bottom: Pixels::ZERO,
4980 left: ScrollbarLayout::BORDER_WIDTH,
4981 },
4982 cx.theme().colors().scrollbar_thumb_border,
4983 ));
4984 })
4985 }
4986
4987 window.set_cursor_style(CursorStyle::Arrow, &hitbox);
4988
4989 window.on_mouse_event({
4990 let editor = self.editor.clone();
4991
4992 // there may be a way to avoid this clone
4993 let hitbox = hitbox.clone();
4994
4995 let mut mouse_position = window.mouse_position();
4996 move |event: &MouseMoveEvent, phase, window, cx| {
4997 if phase == DispatchPhase::Capture {
4998 return;
4999 }
5000
5001 editor.update(cx, |editor, cx| {
5002 if event.pressed_button == Some(MouseButton::Left)
5003 && editor
5004 .scroll_manager
5005 .is_dragging_scrollbar(Axis::Horizontal)
5006 {
5007 let x = mouse_position.x;
5008 let new_x = event.position.x;
5009 if (hitbox.left()..hitbox.right()).contains(&x) {
5010 let mut position = editor.scroll_position(cx);
5011
5012 position.x += (new_x - x) / text_unit_size;
5013 if position.x < 0.0 {
5014 position.x = 0.0;
5015 }
5016 editor.set_scroll_position(position, window, cx);
5017 }
5018
5019 cx.stop_propagation();
5020 } else {
5021 editor.scroll_manager.set_is_dragging_scrollbar(
5022 Axis::Horizontal,
5023 false,
5024 cx,
5025 );
5026
5027 if hitbox.is_hovered(window) {
5028 editor.scroll_manager.show_scrollbar(window, cx);
5029 }
5030 }
5031 mouse_position = event.position;
5032 })
5033 }
5034 });
5035
5036 if self
5037 .editor
5038 .read(cx)
5039 .scroll_manager
5040 .is_dragging_scrollbar(Axis::Horizontal)
5041 {
5042 window.on_mouse_event({
5043 let editor = self.editor.clone();
5044 move |_: &MouseUpEvent, phase, _, cx| {
5045 if phase == DispatchPhase::Capture {
5046 return;
5047 }
5048
5049 editor.update(cx, |editor, cx| {
5050 editor.scroll_manager.set_is_dragging_scrollbar(
5051 Axis::Horizontal,
5052 false,
5053 cx,
5054 );
5055 cx.stop_propagation();
5056 });
5057 }
5058 });
5059 } else {
5060 window.on_mouse_event({
5061 let editor = self.editor.clone();
5062
5063 move |event: &MouseDownEvent, phase, window, cx| {
5064 if phase == DispatchPhase::Capture || !hitbox.is_hovered(window) {
5065 return;
5066 }
5067
5068 editor.update(cx, |editor, cx| {
5069 editor.scroll_manager.set_is_dragging_scrollbar(
5070 Axis::Horizontal,
5071 true,
5072 cx,
5073 );
5074
5075 let x = event.position.x;
5076
5077 if x < thumb_bounds.left() || thumb_bounds.right() < x {
5078 let center_row =
5079 ((x - hitbox.left()) / text_unit_size).round() as u32;
5080 let top_row = center_row.saturating_sub(
5081 (visible_range.end - visible_range.start) as u32 / 2,
5082 );
5083
5084 let mut position = editor.scroll_position(cx);
5085 position.x = top_row as f32;
5086
5087 editor.set_scroll_position(position, window, cx);
5088 } else {
5089 editor.scroll_manager.show_scrollbar(window, cx);
5090 }
5091
5092 cx.stop_propagation();
5093 });
5094 }
5095 });
5096 }
5097 }
5098
5099 if let Some(scrollbar_layout) = scrollbar_y {
5100 let hitbox = scrollbar_layout.hitbox.clone();
5101 let text_unit_size = scrollbar_layout.text_unit_size;
5102 let visible_range = scrollbar_layout.visible_range.clone();
5103 let thumb_bounds = scrollbar_layout.thumb_bounds();
5104
5105 if scrollbar_layout.visible {
5106 window.paint_layer(hitbox.bounds, |window| {
5107 window.paint_quad(quad(
5108 hitbox.bounds,
5109 Corners::default(),
5110 cx.theme().colors().scrollbar_track_background,
5111 Edges {
5112 top: Pixels::ZERO,
5113 right: Pixels::ZERO,
5114 bottom: Pixels::ZERO,
5115 left: ScrollbarLayout::BORDER_WIDTH,
5116 },
5117 cx.theme().colors().scrollbar_track_border,
5118 ));
5119
5120 let fast_markers =
5121 self.collect_fast_scrollbar_markers(layout, &scrollbar_layout, cx);
5122 // Refresh slow scrollbar markers in the background. Below, we paint whatever markers have already been computed.
5123 self.refresh_slow_scrollbar_markers(layout, &scrollbar_layout, window, cx);
5124
5125 let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone();
5126 for marker in markers.iter().chain(&fast_markers) {
5127 let mut marker = marker.clone();
5128 marker.bounds.origin += hitbox.origin;
5129 window.paint_quad(marker);
5130 }
5131
5132 window.paint_quad(quad(
5133 thumb_bounds,
5134 Corners::default(),
5135 cx.theme().colors().scrollbar_thumb_background,
5136 Edges {
5137 top: Pixels::ZERO,
5138 right: Pixels::ZERO,
5139 bottom: Pixels::ZERO,
5140 left: ScrollbarLayout::BORDER_WIDTH,
5141 },
5142 cx.theme().colors().scrollbar_thumb_border,
5143 ));
5144 });
5145 }
5146
5147 window.set_cursor_style(CursorStyle::Arrow, &hitbox);
5148
5149 window.on_mouse_event({
5150 let editor = self.editor.clone();
5151
5152 let hitbox = hitbox.clone();
5153
5154 let mut mouse_position = window.mouse_position();
5155 move |event: &MouseMoveEvent, phase, window, cx| {
5156 if phase == DispatchPhase::Capture {
5157 return;
5158 }
5159
5160 editor.update(cx, |editor, cx| {
5161 if event.pressed_button == Some(MouseButton::Left)
5162 && editor.scroll_manager.is_dragging_scrollbar(Axis::Vertical)
5163 {
5164 let y = mouse_position.y;
5165 let new_y = event.position.y;
5166 if (hitbox.top()..hitbox.bottom()).contains(&y) {
5167 let mut position = editor.scroll_position(cx);
5168 position.y += (new_y - y) / text_unit_size;
5169 if position.y < 0.0 {
5170 position.y = 0.0;
5171 }
5172 editor.set_scroll_position(position, window, cx);
5173 }
5174 } else {
5175 editor.scroll_manager.set_is_dragging_scrollbar(
5176 Axis::Vertical,
5177 false,
5178 cx,
5179 );
5180
5181 if hitbox.is_hovered(window) {
5182 editor.scroll_manager.show_scrollbar(window, cx);
5183 }
5184 }
5185 mouse_position = event.position;
5186 })
5187 }
5188 });
5189
5190 if self
5191 .editor
5192 .read(cx)
5193 .scroll_manager
5194 .is_dragging_scrollbar(Axis::Vertical)
5195 {
5196 window.on_mouse_event({
5197 let editor = self.editor.clone();
5198 move |_: &MouseUpEvent, phase, _, cx| {
5199 if phase == DispatchPhase::Capture {
5200 return;
5201 }
5202
5203 editor.update(cx, |editor, cx| {
5204 editor.scroll_manager.set_is_dragging_scrollbar(
5205 Axis::Vertical,
5206 false,
5207 cx,
5208 );
5209 cx.stop_propagation();
5210 });
5211 }
5212 });
5213 } else {
5214 window.on_mouse_event({
5215 let editor = self.editor.clone();
5216
5217 move |event: &MouseDownEvent, phase, window, cx| {
5218 if phase == DispatchPhase::Capture || !hitbox.is_hovered(window) {
5219 return;
5220 }
5221
5222 editor.update(cx, |editor, cx| {
5223 editor.scroll_manager.set_is_dragging_scrollbar(
5224 Axis::Vertical,
5225 true,
5226 cx,
5227 );
5228
5229 let y = event.position.y;
5230 if y < thumb_bounds.top() || thumb_bounds.bottom() < y {
5231 let center_row =
5232 ((y - hitbox.top()) / text_unit_size).round() as u32;
5233 let top_row = center_row.saturating_sub(
5234 (visible_range.end - visible_range.start) as u32 / 2,
5235 );
5236 let mut position = editor.scroll_position(cx);
5237 position.y = top_row as f32;
5238 editor.set_scroll_position(position, window, cx);
5239 } else {
5240 editor.scroll_manager.show_scrollbar(window, cx);
5241 }
5242
5243 cx.stop_propagation();
5244 });
5245 }
5246 });
5247 }
5248 }
5249 }
5250
5251 fn collect_fast_scrollbar_markers(
5252 &self,
5253 layout: &EditorLayout,
5254 scrollbar_layout: &ScrollbarLayout,
5255 cx: &mut App,
5256 ) -> Vec<PaintQuad> {
5257 const LIMIT: usize = 100;
5258 if !EditorSettings::get_global(cx).scrollbar.cursors || layout.cursors.len() > LIMIT {
5259 return vec![];
5260 }
5261 let cursor_ranges = layout
5262 .cursors
5263 .iter()
5264 .map(|(point, color)| ColoredRange {
5265 start: point.row(),
5266 end: point.row(),
5267 color: *color,
5268 })
5269 .collect_vec();
5270 scrollbar_layout.marker_quads_for_ranges(cursor_ranges, None)
5271 }
5272
5273 fn refresh_slow_scrollbar_markers(
5274 &self,
5275 layout: &EditorLayout,
5276 scrollbar_layout: &ScrollbarLayout,
5277 window: &mut Window,
5278 cx: &mut App,
5279 ) {
5280 self.editor.update(cx, |editor, cx| {
5281 if !editor.is_singleton(cx)
5282 || !editor
5283 .scrollbar_marker_state
5284 .should_refresh(scrollbar_layout.hitbox.size)
5285 {
5286 return;
5287 }
5288
5289 let scrollbar_layout = scrollbar_layout.clone();
5290 let background_highlights = editor.background_highlights.clone();
5291 let snapshot = layout.position_map.snapshot.clone();
5292 let theme = cx.theme().clone();
5293 let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
5294
5295 editor.scrollbar_marker_state.dirty = false;
5296 editor.scrollbar_marker_state.pending_refresh =
5297 Some(cx.spawn_in(window, |editor, mut cx| async move {
5298 let scrollbar_size = scrollbar_layout.hitbox.size;
5299 let scrollbar_markers = cx
5300 .background_spawn(async move {
5301 let max_point = snapshot.display_snapshot.buffer_snapshot.max_point();
5302 let mut marker_quads = Vec::new();
5303 if scrollbar_settings.git_diff {
5304 let marker_row_ranges =
5305 snapshot.buffer_snapshot.diff_hunks().map(|hunk| {
5306 let start_display_row =
5307 MultiBufferPoint::new(hunk.row_range.start.0, 0)
5308 .to_display_point(&snapshot.display_snapshot)
5309 .row();
5310 let mut end_display_row =
5311 MultiBufferPoint::new(hunk.row_range.end.0, 0)
5312 .to_display_point(&snapshot.display_snapshot)
5313 .row();
5314 if end_display_row != start_display_row {
5315 end_display_row.0 -= 1;
5316 }
5317 let color = match &hunk.status().kind {
5318 DiffHunkStatusKind::Added => theme.status().created,
5319 DiffHunkStatusKind::Modified => theme.status().modified,
5320 DiffHunkStatusKind::Deleted => theme.status().deleted,
5321 };
5322 ColoredRange {
5323 start: start_display_row,
5324 end: end_display_row,
5325 color,
5326 }
5327 });
5328
5329 marker_quads.extend(
5330 scrollbar_layout
5331 .marker_quads_for_ranges(marker_row_ranges, Some(0)),
5332 );
5333 }
5334
5335 for (background_highlight_id, (_, background_ranges)) in
5336 background_highlights.iter()
5337 {
5338 let is_search_highlights = *background_highlight_id
5339 == TypeId::of::<BufferSearchHighlights>();
5340 let is_text_highlights = *background_highlight_id
5341 == TypeId::of::<SelectedTextHighlight>();
5342 let is_symbol_occurrences = *background_highlight_id
5343 == TypeId::of::<DocumentHighlightRead>()
5344 || *background_highlight_id
5345 == TypeId::of::<DocumentHighlightWrite>();
5346 if (is_search_highlights && scrollbar_settings.search_results)
5347 || (is_text_highlights && scrollbar_settings.selected_text)
5348 || (is_symbol_occurrences && scrollbar_settings.selected_symbol)
5349 {
5350 let mut color = theme.status().info;
5351 if is_symbol_occurrences {
5352 color.fade_out(0.5);
5353 }
5354 let marker_row_ranges = background_ranges.iter().map(|range| {
5355 let display_start = range
5356 .start
5357 .to_display_point(&snapshot.display_snapshot);
5358 let display_end =
5359 range.end.to_display_point(&snapshot.display_snapshot);
5360 ColoredRange {
5361 start: display_start.row(),
5362 end: display_end.row(),
5363 color,
5364 }
5365 });
5366 marker_quads.extend(
5367 scrollbar_layout
5368 .marker_quads_for_ranges(marker_row_ranges, Some(1)),
5369 );
5370 }
5371 }
5372
5373 if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
5374 let diagnostics = snapshot
5375 .buffer_snapshot
5376 .diagnostics_in_range::<Point>(Point::zero()..max_point)
5377 // Don't show diagnostics the user doesn't care about
5378 .filter(|diagnostic| {
5379 match (
5380 scrollbar_settings.diagnostics,
5381 diagnostic.diagnostic.severity,
5382 ) {
5383 (ScrollbarDiagnostics::All, _) => true,
5384 (
5385 ScrollbarDiagnostics::Error,
5386 DiagnosticSeverity::ERROR,
5387 ) => true,
5388 (
5389 ScrollbarDiagnostics::Warning,
5390 DiagnosticSeverity::ERROR
5391 | DiagnosticSeverity::WARNING,
5392 ) => true,
5393 (
5394 ScrollbarDiagnostics::Information,
5395 DiagnosticSeverity::ERROR
5396 | DiagnosticSeverity::WARNING
5397 | DiagnosticSeverity::INFORMATION,
5398 ) => true,
5399 (_, _) => false,
5400 }
5401 })
5402 // We want to sort by severity, in order to paint the most severe diagnostics last.
5403 .sorted_by_key(|diagnostic| {
5404 std::cmp::Reverse(diagnostic.diagnostic.severity)
5405 });
5406
5407 let marker_row_ranges = diagnostics.into_iter().map(|diagnostic| {
5408 let start_display = diagnostic
5409 .range
5410 .start
5411 .to_display_point(&snapshot.display_snapshot);
5412 let end_display = diagnostic
5413 .range
5414 .end
5415 .to_display_point(&snapshot.display_snapshot);
5416 let color = match diagnostic.diagnostic.severity {
5417 DiagnosticSeverity::ERROR => theme.status().error,
5418 DiagnosticSeverity::WARNING => theme.status().warning,
5419 DiagnosticSeverity::INFORMATION => theme.status().info,
5420 _ => theme.status().hint,
5421 };
5422 ColoredRange {
5423 start: start_display.row(),
5424 end: end_display.row(),
5425 color,
5426 }
5427 });
5428 marker_quads.extend(
5429 scrollbar_layout
5430 .marker_quads_for_ranges(marker_row_ranges, Some(2)),
5431 );
5432 }
5433
5434 Arc::from(marker_quads)
5435 })
5436 .await;
5437
5438 editor.update(&mut cx, |editor, cx| {
5439 editor.scrollbar_marker_state.markers = scrollbar_markers;
5440 editor.scrollbar_marker_state.scrollbar_size = scrollbar_size;
5441 editor.scrollbar_marker_state.pending_refresh = None;
5442 cx.notify();
5443 })?;
5444
5445 Ok(())
5446 }));
5447 });
5448 }
5449
5450 #[allow(clippy::too_many_arguments)]
5451 fn paint_highlighted_range(
5452 &self,
5453 range: Range<DisplayPoint>,
5454 color: Hsla,
5455 corner_radius: Pixels,
5456 line_end_overshoot: Pixels,
5457 layout: &EditorLayout,
5458 window: &mut Window,
5459 ) {
5460 let start_row = layout.visible_display_row_range.start;
5461 let end_row = layout.visible_display_row_range.end;
5462 if range.start != range.end {
5463 let row_range = if range.end.column() == 0 {
5464 cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
5465 } else {
5466 cmp::max(range.start.row(), start_row)
5467 ..cmp::min(range.end.row().next_row(), end_row)
5468 };
5469
5470 let highlighted_range = HighlightedRange {
5471 color,
5472 line_height: layout.position_map.line_height,
5473 corner_radius,
5474 start_y: layout.content_origin.y
5475 + row_range.start.as_f32() * layout.position_map.line_height
5476 - layout.position_map.scroll_pixel_position.y,
5477 lines: row_range
5478 .iter_rows()
5479 .map(|row| {
5480 let line_layout =
5481 &layout.position_map.line_layouts[row.minus(start_row) as usize];
5482 HighlightedRangeLine {
5483 start_x: if row == range.start.row() {
5484 layout.content_origin.x
5485 + line_layout.x_for_index(range.start.column() as usize)
5486 - layout.position_map.scroll_pixel_position.x
5487 } else {
5488 layout.content_origin.x
5489 - layout.position_map.scroll_pixel_position.x
5490 },
5491 end_x: if row == range.end.row() {
5492 layout.content_origin.x
5493 + line_layout.x_for_index(range.end.column() as usize)
5494 - layout.position_map.scroll_pixel_position.x
5495 } else {
5496 layout.content_origin.x + line_layout.width + line_end_overshoot
5497 - layout.position_map.scroll_pixel_position.x
5498 },
5499 }
5500 })
5501 .collect(),
5502 };
5503
5504 highlighted_range.paint(layout.position_map.text_hitbox.bounds, window);
5505 }
5506 }
5507
5508 fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
5509 if let Some(mut inline_blame) = layout.inline_blame.take() {
5510 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
5511 inline_blame.paint(window, cx);
5512 })
5513 }
5514 }
5515
5516 fn paint_diff_hunk_controls(
5517 &mut self,
5518 layout: &mut EditorLayout,
5519 window: &mut Window,
5520 cx: &mut App,
5521 ) {
5522 for mut diff_hunk_control in layout.diff_hunk_controls.drain(..) {
5523 diff_hunk_control.paint(window, cx);
5524 }
5525 }
5526
5527 fn paint_blocks(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
5528 for mut block in layout.blocks.drain(..) {
5529 block.element.paint(window, cx);
5530 }
5531 }
5532
5533 fn paint_inline_completion_popover(
5534 &mut self,
5535 layout: &mut EditorLayout,
5536 window: &mut Window,
5537 cx: &mut App,
5538 ) {
5539 if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() {
5540 inline_completion_popover.paint(window, cx);
5541 }
5542 }
5543
5544 fn paint_mouse_context_menu(
5545 &mut self,
5546 layout: &mut EditorLayout,
5547 window: &mut Window,
5548 cx: &mut App,
5549 ) {
5550 if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
5551 mouse_context_menu.paint(window, cx);
5552 }
5553 }
5554
5555 fn paint_scroll_wheel_listener(
5556 &mut self,
5557 layout: &EditorLayout,
5558 window: &mut Window,
5559 cx: &mut App,
5560 ) {
5561 window.on_mouse_event({
5562 let position_map = layout.position_map.clone();
5563 let editor = self.editor.clone();
5564 let hitbox = layout.hitbox.clone();
5565 let mut delta = ScrollDelta::default();
5566
5567 // Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't
5568 // accidentally turn off their scrolling.
5569 let scroll_sensitivity = EditorSettings::get_global(cx).scroll_sensitivity.max(0.01);
5570
5571 move |event: &ScrollWheelEvent, phase, window, cx| {
5572 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
5573 delta = delta.coalesce(event.delta);
5574 editor.update(cx, |editor, cx| {
5575 let position_map: &PositionMap = &position_map;
5576
5577 let line_height = position_map.line_height;
5578 let max_glyph_width = position_map.em_width;
5579 let (delta, axis) = match delta {
5580 gpui::ScrollDelta::Pixels(mut pixels) => {
5581 //Trackpad
5582 let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
5583 (pixels, axis)
5584 }
5585
5586 gpui::ScrollDelta::Lines(lines) => {
5587 //Not trackpad
5588 let pixels =
5589 point(lines.x * max_glyph_width, lines.y * line_height);
5590 (pixels, None)
5591 }
5592 };
5593
5594 let current_scroll_position = position_map.snapshot.scroll_position();
5595 let x = (current_scroll_position.x * max_glyph_width
5596 - (delta.x * scroll_sensitivity))
5597 / max_glyph_width;
5598 let y = (current_scroll_position.y * line_height
5599 - (delta.y * scroll_sensitivity))
5600 / line_height;
5601 let mut scroll_position =
5602 point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
5603 let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll();
5604 if forbid_vertical_scroll {
5605 scroll_position.y = current_scroll_position.y;
5606 }
5607
5608 if scroll_position != current_scroll_position {
5609 editor.scroll(scroll_position, axis, window, cx);
5610 cx.stop_propagation();
5611 } else if y < 0. {
5612 // Due to clamping, we may fail to detect cases of overscroll to the top;
5613 // We want the scroll manager to get an update in such cases and detect the change of direction
5614 // on the next frame.
5615 cx.notify();
5616 }
5617 });
5618 }
5619 }
5620 });
5621 }
5622
5623 fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
5624 self.paint_scroll_wheel_listener(layout, window, cx);
5625
5626 window.on_mouse_event({
5627 let position_map = layout.position_map.clone();
5628 let editor = self.editor.clone();
5629 let diff_hunk_range =
5630 layout
5631 .display_hunks
5632 .iter()
5633 .find_map(|(hunk, hunk_hitbox)| match hunk {
5634 DisplayDiffHunk::Folded { .. } => None,
5635 DisplayDiffHunk::Unfolded {
5636 multi_buffer_range, ..
5637 } => {
5638 if hunk_hitbox
5639 .as_ref()
5640 .map(|hitbox| hitbox.is_hovered(window))
5641 .unwrap_or(false)
5642 {
5643 Some(multi_buffer_range.clone())
5644 } else {
5645 None
5646 }
5647 }
5648 });
5649 let line_numbers = layout.line_numbers.clone();
5650
5651 move |event: &MouseDownEvent, phase, window, cx| {
5652 if phase == DispatchPhase::Bubble {
5653 match event.button {
5654 MouseButton::Left => editor.update(cx, |editor, cx| {
5655 let pending_mouse_down = editor
5656 .pending_mouse_down
5657 .get_or_insert_with(Default::default)
5658 .clone();
5659
5660 *pending_mouse_down.borrow_mut() = Some(event.clone());
5661
5662 Self::mouse_left_down(
5663 editor,
5664 event,
5665 diff_hunk_range.clone(),
5666 &position_map,
5667 line_numbers.as_ref(),
5668 window,
5669 cx,
5670 );
5671 }),
5672 MouseButton::Right => editor.update(cx, |editor, cx| {
5673 Self::mouse_right_down(editor, event, &position_map, window, cx);
5674 }),
5675 MouseButton::Middle => editor.update(cx, |editor, cx| {
5676 Self::mouse_middle_down(editor, event, &position_map, window, cx);
5677 }),
5678 _ => {}
5679 };
5680 }
5681 }
5682 });
5683
5684 window.on_mouse_event({
5685 let editor = self.editor.clone();
5686 let position_map = layout.position_map.clone();
5687
5688 move |event: &MouseUpEvent, phase, window, cx| {
5689 if phase == DispatchPhase::Bubble {
5690 editor.update(cx, |editor, cx| {
5691 Self::mouse_up(editor, event, &position_map, window, cx)
5692 });
5693 }
5694 }
5695 });
5696
5697 window.on_mouse_event({
5698 let editor = self.editor.clone();
5699 let position_map = layout.position_map.clone();
5700 let mut captured_mouse_down = None;
5701
5702 move |event: &MouseUpEvent, phase, window, cx| match phase {
5703 // Clear the pending mouse down during the capture phase,
5704 // so that it happens even if another event handler stops
5705 // propagation.
5706 DispatchPhase::Capture => editor.update(cx, |editor, _cx| {
5707 let pending_mouse_down = editor
5708 .pending_mouse_down
5709 .get_or_insert_with(Default::default)
5710 .clone();
5711
5712 let mut pending_mouse_down = pending_mouse_down.borrow_mut();
5713 if pending_mouse_down.is_some() && position_map.text_hitbox.is_hovered(window) {
5714 captured_mouse_down = pending_mouse_down.take();
5715 window.refresh();
5716 }
5717 }),
5718 // Fire click handlers during the bubble phase.
5719 DispatchPhase::Bubble => editor.update(cx, |editor, cx| {
5720 if let Some(mouse_down) = captured_mouse_down.take() {
5721 let event = ClickEvent {
5722 down: mouse_down,
5723 up: event.clone(),
5724 };
5725 Self::click(editor, &event, &position_map, window, cx);
5726 }
5727 }),
5728 }
5729 });
5730
5731 window.on_mouse_event({
5732 let position_map = layout.position_map.clone();
5733 let editor = self.editor.clone();
5734
5735 move |event: &MouseMoveEvent, phase, window, cx| {
5736 if phase == DispatchPhase::Bubble {
5737 editor.update(cx, |editor, cx| {
5738 if editor.hover_state.focused(window, cx) {
5739 return;
5740 }
5741 if event.pressed_button == Some(MouseButton::Left)
5742 || event.pressed_button == Some(MouseButton::Middle)
5743 {
5744 Self::mouse_dragged(editor, event, &position_map, window, cx)
5745 }
5746
5747 Self::mouse_moved(editor, event, &position_map, window, cx)
5748 });
5749 }
5750 }
5751 });
5752 }
5753
5754 fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels {
5755 bounds.top_right().x - self.style.scrollbar_width
5756 }
5757
5758 fn column_pixels(&self, column: usize, window: &mut Window, _: &mut App) -> Pixels {
5759 let style = &self.style;
5760 let font_size = style.text.font_size.to_pixels(window.rem_size());
5761 let layout = window
5762 .text_system()
5763 .shape_line(
5764 SharedString::from(" ".repeat(column)),
5765 font_size,
5766 &[TextRun {
5767 len: column,
5768 font: style.text.font(),
5769 color: Hsla::default(),
5770 background_color: None,
5771 underline: None,
5772 strikethrough: None,
5773 }],
5774 )
5775 .unwrap();
5776
5777 layout.width
5778 }
5779
5780 fn max_line_number_width(
5781 &self,
5782 snapshot: &EditorSnapshot,
5783 window: &mut Window,
5784 cx: &mut App,
5785 ) -> Pixels {
5786 let digit_count = (snapshot.widest_line_number() as f32).log10().floor() as usize + 1;
5787 self.column_pixels(digit_count, window, cx)
5788 }
5789
5790 fn shape_line_number(
5791 &self,
5792 text: SharedString,
5793 color: Hsla,
5794 window: &mut Window,
5795 ) -> anyhow::Result<ShapedLine> {
5796 let run = TextRun {
5797 len: text.len(),
5798 font: self.style.text.font(),
5799 color,
5800 background_color: None,
5801 underline: None,
5802 strikethrough: None,
5803 };
5804 window.text_system().shape_line(
5805 text,
5806 self.style.text.font_size.to_pixels(window.rem_size()),
5807 &[run],
5808 )
5809 }
5810}
5811
5812fn header_jump_data(
5813 snapshot: &EditorSnapshot,
5814 block_row_start: DisplayRow,
5815 height: u32,
5816 for_excerpt: &ExcerptInfo,
5817) -> JumpData {
5818 let range = &for_excerpt.range;
5819 let buffer = &for_excerpt.buffer;
5820 let jump_anchor = range
5821 .primary
5822 .as_ref()
5823 .map_or(range.context.start, |primary| primary.start);
5824
5825 let excerpt_start = range.context.start;
5826 let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
5827 let rows_from_excerpt_start = if jump_anchor == excerpt_start {
5828 0
5829 } else {
5830 let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
5831 jump_position.row.saturating_sub(excerpt_start_point.row)
5832 };
5833
5834 let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start)
5835 .saturating_sub(
5836 snapshot
5837 .scroll_anchor
5838 .scroll_position(&snapshot.display_snapshot)
5839 .y as u32,
5840 );
5841
5842 JumpData::MultiBufferPoint {
5843 excerpt_id: for_excerpt.id,
5844 anchor: jump_anchor,
5845 position: jump_position,
5846 line_offset_from_top,
5847 }
5848}
5849
5850pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
5851
5852impl AcceptEditPredictionBinding {
5853 pub fn keystroke(&self) -> Option<&Keystroke> {
5854 if let Some(binding) = self.0.as_ref() {
5855 match &binding.keystrokes() {
5856 [keystroke] => Some(keystroke),
5857 _ => None,
5858 }
5859 } else {
5860 None
5861 }
5862 }
5863}
5864
5865#[allow(clippy::too_many_arguments)]
5866fn prepaint_gutter_button(
5867 button: IconButton,
5868 row: DisplayRow,
5869 line_height: Pixels,
5870 gutter_dimensions: &GutterDimensions,
5871 scroll_pixel_position: gpui::Point<Pixels>,
5872 gutter_hitbox: &Hitbox,
5873 rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
5874 window: &mut Window,
5875 cx: &mut App,
5876) -> AnyElement {
5877 let mut button = button.into_any_element();
5878 let available_space = size(
5879 AvailableSpace::MinContent,
5880 AvailableSpace::Definite(line_height),
5881 );
5882 let indicator_size = button.layout_as_root(available_space, window, cx);
5883
5884 let blame_width = gutter_dimensions.git_blame_entries_width;
5885 let gutter_width = rows_with_hunk_bounds
5886 .get(&row)
5887 .map(|bounds| bounds.size.width);
5888 let left_offset = blame_width.max(gutter_width).unwrap_or_default();
5889
5890 let mut x = left_offset;
5891 let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
5892 - indicator_size.width
5893 - left_offset;
5894 x += available_width / 2.;
5895
5896 let mut y = row.as_f32() * line_height - scroll_pixel_position.y;
5897 y += (line_height - indicator_size.height) / 2.;
5898
5899 button.prepaint_as_root(
5900 gutter_hitbox.origin + point(x, y),
5901 available_space,
5902 window,
5903 cx,
5904 );
5905 button
5906}
5907
5908fn render_inline_blame_entry(
5909 editor: Entity<Editor>,
5910 blame: &gpui::Entity<GitBlame>,
5911 blame_entry: BlameEntry,
5912 style: &EditorStyle,
5913 cx: &mut App,
5914) -> AnyElement {
5915 let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
5916
5917 let author = blame_entry.author.as_deref().unwrap_or_default();
5918 let summary_enabled = ProjectSettings::get_global(cx)
5919 .git
5920 .show_inline_commit_summary();
5921
5922 let text = match blame_entry.summary.as_ref() {
5923 Some(summary) if summary_enabled => {
5924 format!("{}, {} - {}", author, relative_timestamp, summary)
5925 }
5926 _ => format!("{}, {}", author, relative_timestamp),
5927 };
5928 let blame = blame.clone();
5929 let blame_entry = blame_entry.clone();
5930
5931 h_flex()
5932 .id("inline-blame")
5933 .w_full()
5934 .font_family(style.text.font().family)
5935 .text_color(cx.theme().status().hint)
5936 .line_height(style.text.line_height)
5937 .child(Icon::new(IconName::FileGit).color(Color::Hint))
5938 .child(text)
5939 .gap_2()
5940 .hoverable_tooltip(move |window, cx| {
5941 let details = blame.read(cx).details_for_entry(&blame_entry);
5942 let tooltip =
5943 cx.new(|cx| CommitTooltip::blame_entry(&blame_entry, details, window, cx));
5944 editor.update(cx, |editor, _| {
5945 editor.git_blame_inline_tooltip = Some(tooltip.downgrade())
5946 });
5947 tooltip.into()
5948 })
5949 .into_any()
5950}
5951
5952fn render_blame_entry(
5953 ix: usize,
5954 blame: &gpui::Entity<GitBlame>,
5955 blame_entry: BlameEntry,
5956 style: &EditorStyle,
5957 last_used_color: &mut Option<(PlayerColor, Oid)>,
5958 editor: Entity<Editor>,
5959 cx: &mut App,
5960) -> AnyElement {
5961 let mut sha_color = cx
5962 .theme()
5963 .players()
5964 .color_for_participant(blame_entry.sha.into());
5965 // If the last color we used is the same as the one we get for this line, but
5966 // the commit SHAs are different, then we try again to get a different color.
5967 match *last_used_color {
5968 Some((color, sha)) if sha != blame_entry.sha && color.cursor == sha_color.cursor => {
5969 let index: u32 = blame_entry.sha.into();
5970 sha_color = cx.theme().players().color_for_participant(index + 1);
5971 }
5972 _ => {}
5973 };
5974 last_used_color.replace((sha_color, blame_entry.sha));
5975
5976 let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
5977
5978 let short_commit_id = blame_entry.sha.display_short();
5979
5980 let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
5981 let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
5982 let details = blame.read(cx).details_for_entry(&blame_entry);
5983
5984 h_flex()
5985 .w_full()
5986 .justify_between()
5987 .font_family(style.text.font().family)
5988 .line_height(style.text.line_height)
5989 .id(("blame", ix))
5990 .text_color(cx.theme().status().hint)
5991 .pr_2()
5992 .gap_2()
5993 .child(
5994 h_flex()
5995 .items_center()
5996 .gap_2()
5997 .child(div().text_color(sha_color.cursor).child(short_commit_id))
5998 .child(name),
5999 )
6000 .child(relative_timestamp)
6001 .on_mouse_down(MouseButton::Right, {
6002 let blame_entry = blame_entry.clone();
6003 let details = details.clone();
6004 move |event, window, cx| {
6005 deploy_blame_entry_context_menu(
6006 &blame_entry,
6007 details.as_ref(),
6008 editor.clone(),
6009 event.position,
6010 window,
6011 cx,
6012 );
6013 }
6014 })
6015 .hover(|style| style.bg(cx.theme().colors().element_hover))
6016 .when_some(
6017 details
6018 .as_ref()
6019 .and_then(|details| details.permalink.clone()),
6020 |this, url| {
6021 this.cursor_pointer().on_click(move |_, _, cx| {
6022 cx.stop_propagation();
6023 cx.open_url(url.as_str())
6024 })
6025 },
6026 )
6027 .hoverable_tooltip(move |window, cx| {
6028 cx.new(|cx| CommitTooltip::blame_entry(&blame_entry, details.clone(), window, cx))
6029 .into()
6030 })
6031 .into_any()
6032}
6033
6034fn deploy_blame_entry_context_menu(
6035 blame_entry: &BlameEntry,
6036 details: Option<&ParsedCommitMessage>,
6037 editor: Entity<Editor>,
6038 position: gpui::Point<Pixels>,
6039 window: &mut Window,
6040 cx: &mut App,
6041) {
6042 let context_menu = ContextMenu::build(window, cx, move |menu, _, _| {
6043 let sha = format!("{}", blame_entry.sha);
6044 menu.on_blur_subscription(Subscription::new(|| {}))
6045 .entry("Copy commit SHA", None, move |_, cx| {
6046 cx.write_to_clipboard(ClipboardItem::new_string(sha.clone()));
6047 })
6048 .when_some(
6049 details.and_then(|details| details.permalink.clone()),
6050 |this, url| {
6051 this.entry("Open permalink", None, move |_, cx| {
6052 cx.open_url(url.as_str())
6053 })
6054 },
6055 )
6056 });
6057
6058 editor.update(cx, move |editor, cx| {
6059 editor.mouse_context_menu = Some(MouseContextMenu::new(
6060 MenuPosition::PinnedToScreen(position),
6061 context_menu,
6062 window,
6063 cx,
6064 ));
6065 cx.notify();
6066 });
6067}
6068
6069#[derive(Debug)]
6070pub(crate) struct LineWithInvisibles {
6071 fragments: SmallVec<[LineFragment; 1]>,
6072 invisibles: Vec<Invisible>,
6073 len: usize,
6074 width: Pixels,
6075 font_size: Pixels,
6076}
6077
6078#[allow(clippy::large_enum_variant)]
6079enum LineFragment {
6080 Text(ShapedLine),
6081 Element {
6082 element: Option<AnyElement>,
6083 size: Size<Pixels>,
6084 len: usize,
6085 },
6086}
6087
6088impl fmt::Debug for LineFragment {
6089 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6090 match self {
6091 LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
6092 LineFragment::Element { size, len, .. } => f
6093 .debug_struct("Element")
6094 .field("size", size)
6095 .field("len", len)
6096 .finish(),
6097 }
6098 }
6099}
6100
6101impl LineWithInvisibles {
6102 #[allow(clippy::too_many_arguments)]
6103 fn from_chunks<'a>(
6104 chunks: impl Iterator<Item = HighlightedChunk<'a>>,
6105 editor_style: &EditorStyle,
6106 max_line_len: usize,
6107 max_line_count: usize,
6108 editor_mode: EditorMode,
6109 text_width: Pixels,
6110 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
6111 window: &mut Window,
6112 cx: &mut App,
6113 ) -> Vec<Self> {
6114 let text_style = &editor_style.text;
6115 let mut layouts = Vec::with_capacity(max_line_count);
6116 let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
6117 let mut line = String::new();
6118 let mut invisibles = Vec::new();
6119 let mut width = Pixels::ZERO;
6120 let mut len = 0;
6121 let mut styles = Vec::new();
6122 let mut non_whitespace_added = false;
6123 let mut row = 0;
6124 let mut line_exceeded_max_len = false;
6125 let font_size = text_style.font_size.to_pixels(window.rem_size());
6126
6127 let ellipsis = SharedString::from("⋯");
6128
6129 for highlighted_chunk in chunks.chain([HighlightedChunk {
6130 text: "\n",
6131 style: None,
6132 is_tab: false,
6133 replacement: None,
6134 }]) {
6135 if let Some(replacement) = highlighted_chunk.replacement {
6136 if !line.is_empty() {
6137 let shaped_line = window
6138 .text_system()
6139 .shape_line(line.clone().into(), font_size, &styles)
6140 .unwrap();
6141 width += shaped_line.width;
6142 len += shaped_line.len;
6143 fragments.push(LineFragment::Text(shaped_line));
6144 line.clear();
6145 styles.clear();
6146 }
6147
6148 match replacement {
6149 ChunkReplacement::Renderer(renderer) => {
6150 let available_width = if renderer.constrain_width {
6151 let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
6152 ellipsis.clone()
6153 } else {
6154 SharedString::from(Arc::from(highlighted_chunk.text))
6155 };
6156 let shaped_line = window
6157 .text_system()
6158 .shape_line(
6159 chunk,
6160 font_size,
6161 &[text_style.to_run(highlighted_chunk.text.len())],
6162 )
6163 .unwrap();
6164 AvailableSpace::Definite(shaped_line.width)
6165 } else {
6166 AvailableSpace::MinContent
6167 };
6168
6169 let mut element = (renderer.render)(&mut ChunkRendererContext {
6170 context: cx,
6171 window,
6172 max_width: text_width,
6173 });
6174 let line_height = text_style.line_height_in_pixels(window.rem_size());
6175 let size = element.layout_as_root(
6176 size(available_width, AvailableSpace::Definite(line_height)),
6177 window,
6178 cx,
6179 );
6180
6181 width += size.width;
6182 len += highlighted_chunk.text.len();
6183 fragments.push(LineFragment::Element {
6184 element: Some(element),
6185 size,
6186 len: highlighted_chunk.text.len(),
6187 });
6188 }
6189 ChunkReplacement::Str(x) => {
6190 let text_style = if let Some(style) = highlighted_chunk.style {
6191 Cow::Owned(text_style.clone().highlight(style))
6192 } else {
6193 Cow::Borrowed(text_style)
6194 };
6195
6196 let run = TextRun {
6197 len: x.len(),
6198 font: text_style.font(),
6199 color: text_style.color,
6200 background_color: text_style.background_color,
6201 underline: text_style.underline,
6202 strikethrough: text_style.strikethrough,
6203 };
6204 let line_layout = window
6205 .text_system()
6206 .shape_line(x, font_size, &[run])
6207 .unwrap()
6208 .with_len(highlighted_chunk.text.len());
6209
6210 width += line_layout.width;
6211 len += highlighted_chunk.text.len();
6212 fragments.push(LineFragment::Text(line_layout))
6213 }
6214 }
6215 } else {
6216 for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
6217 if ix > 0 {
6218 let shaped_line = window
6219 .text_system()
6220 .shape_line(line.clone().into(), font_size, &styles)
6221 .unwrap();
6222 width += shaped_line.width;
6223 len += shaped_line.len;
6224 fragments.push(LineFragment::Text(shaped_line));
6225 layouts.push(Self {
6226 width: mem::take(&mut width),
6227 len: mem::take(&mut len),
6228 fragments: mem::take(&mut fragments),
6229 invisibles: std::mem::take(&mut invisibles),
6230 font_size,
6231 });
6232
6233 line.clear();
6234 styles.clear();
6235 row += 1;
6236 line_exceeded_max_len = false;
6237 non_whitespace_added = false;
6238 if row == max_line_count {
6239 return layouts;
6240 }
6241 }
6242
6243 if !line_chunk.is_empty() && !line_exceeded_max_len {
6244 let text_style = if let Some(style) = highlighted_chunk.style {
6245 Cow::Owned(text_style.clone().highlight(style))
6246 } else {
6247 Cow::Borrowed(text_style)
6248 };
6249
6250 if line.len() + line_chunk.len() > max_line_len {
6251 let mut chunk_len = max_line_len - line.len();
6252 while !line_chunk.is_char_boundary(chunk_len) {
6253 chunk_len -= 1;
6254 }
6255 line_chunk = &line_chunk[..chunk_len];
6256 line_exceeded_max_len = true;
6257 }
6258
6259 styles.push(TextRun {
6260 len: line_chunk.len(),
6261 font: text_style.font(),
6262 color: text_style.color,
6263 background_color: text_style.background_color,
6264 underline: text_style.underline,
6265 strikethrough: text_style.strikethrough,
6266 });
6267
6268 if editor_mode == EditorMode::Full {
6269 // Line wrap pads its contents with fake whitespaces,
6270 // avoid printing them
6271 let is_soft_wrapped = is_row_soft_wrapped(row);
6272 if highlighted_chunk.is_tab {
6273 if non_whitespace_added || !is_soft_wrapped {
6274 invisibles.push(Invisible::Tab {
6275 line_start_offset: line.len(),
6276 line_end_offset: line.len() + line_chunk.len(),
6277 });
6278 }
6279 } else {
6280 invisibles.extend(line_chunk.char_indices().filter_map(
6281 |(index, c)| {
6282 let is_whitespace = c.is_whitespace();
6283 non_whitespace_added |= !is_whitespace;
6284 if is_whitespace
6285 && (non_whitespace_added || !is_soft_wrapped)
6286 {
6287 Some(Invisible::Whitespace {
6288 line_offset: line.len() + index,
6289 })
6290 } else {
6291 None
6292 }
6293 },
6294 ))
6295 }
6296 }
6297
6298 line.push_str(line_chunk);
6299 }
6300 }
6301 }
6302 }
6303
6304 layouts
6305 }
6306
6307 #[allow(clippy::too_many_arguments)]
6308 fn prepaint(
6309 &mut self,
6310 line_height: Pixels,
6311 scroll_pixel_position: gpui::Point<Pixels>,
6312 row: DisplayRow,
6313 content_origin: gpui::Point<Pixels>,
6314 line_elements: &mut SmallVec<[AnyElement; 1]>,
6315 window: &mut Window,
6316 cx: &mut App,
6317 ) {
6318 let line_y = line_height * (row.as_f32() - scroll_pixel_position.y / line_height);
6319 let mut fragment_origin = content_origin + gpui::point(-scroll_pixel_position.x, line_y);
6320 for fragment in &mut self.fragments {
6321 match fragment {
6322 LineFragment::Text(line) => {
6323 fragment_origin.x += line.width;
6324 }
6325 LineFragment::Element { element, size, .. } => {
6326 let mut element = element
6327 .take()
6328 .expect("you can't prepaint LineWithInvisibles twice");
6329
6330 // Center the element vertically within the line.
6331 let mut element_origin = fragment_origin;
6332 element_origin.y += (line_height - size.height) / 2.;
6333 element.prepaint_at(element_origin, window, cx);
6334 line_elements.push(element);
6335
6336 fragment_origin.x += size.width;
6337 }
6338 }
6339 }
6340 }
6341
6342 #[allow(clippy::too_many_arguments)]
6343 fn draw(
6344 &self,
6345 layout: &EditorLayout,
6346 row: DisplayRow,
6347 content_origin: gpui::Point<Pixels>,
6348 whitespace_setting: ShowWhitespaceSetting,
6349 selection_ranges: &[Range<DisplayPoint>],
6350 window: &mut Window,
6351 cx: &mut App,
6352 ) {
6353 let line_height = layout.position_map.line_height;
6354 let line_y = line_height
6355 * (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
6356
6357 let mut fragment_origin =
6358 content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
6359
6360 for fragment in &self.fragments {
6361 match fragment {
6362 LineFragment::Text(line) => {
6363 line.paint(fragment_origin, line_height, window, cx)
6364 .log_err();
6365 fragment_origin.x += line.width;
6366 }
6367 LineFragment::Element { size, .. } => {
6368 fragment_origin.x += size.width;
6369 }
6370 }
6371 }
6372
6373 self.draw_invisibles(
6374 selection_ranges,
6375 layout,
6376 content_origin,
6377 line_y,
6378 row,
6379 line_height,
6380 whitespace_setting,
6381 window,
6382 cx,
6383 );
6384 }
6385
6386 #[allow(clippy::too_many_arguments)]
6387 fn draw_invisibles(
6388 &self,
6389 selection_ranges: &[Range<DisplayPoint>],
6390 layout: &EditorLayout,
6391 content_origin: gpui::Point<Pixels>,
6392 line_y: Pixels,
6393 row: DisplayRow,
6394 line_height: Pixels,
6395 whitespace_setting: ShowWhitespaceSetting,
6396 window: &mut Window,
6397 cx: &mut App,
6398 ) {
6399 let extract_whitespace_info = |invisible: &Invisible| {
6400 let (token_offset, token_end_offset, invisible_symbol) = match invisible {
6401 Invisible::Tab {
6402 line_start_offset,
6403 line_end_offset,
6404 } => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
6405 Invisible::Whitespace { line_offset } => {
6406 (*line_offset, line_offset + 1, &layout.space_invisible)
6407 }
6408 };
6409
6410 let x_offset = self.x_for_index(token_offset);
6411 let invisible_offset =
6412 (layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0;
6413 let origin = content_origin
6414 + gpui::point(
6415 x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x,
6416 line_y,
6417 );
6418
6419 (
6420 [token_offset, token_end_offset],
6421 Box::new(move |window: &mut Window, cx: &mut App| {
6422 invisible_symbol
6423 .paint(origin, line_height, window, cx)
6424 .log_err();
6425 }),
6426 )
6427 };
6428
6429 let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
6430 match whitespace_setting {
6431 ShowWhitespaceSetting::None => (),
6432 ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(window, cx)),
6433 ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
6434 let invisible_point = DisplayPoint::new(row, start as u32);
6435 if !selection_ranges
6436 .iter()
6437 .any(|region| region.start <= invisible_point && invisible_point < region.end)
6438 {
6439 return;
6440 }
6441
6442 paint(window, cx);
6443 }),
6444
6445 // For a whitespace to be on a boundary, any of the following conditions need to be met:
6446 // - It is a tab
6447 // - It is adjacent to an edge (start or end)
6448 // - It is adjacent to a whitespace (left or right)
6449 ShowWhitespaceSetting::Boundary => {
6450 // 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
6451 // the above cases.
6452 // Note: We zip in the original `invisibles` to check for tab equality
6453 let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut Window, &mut App)>)> = None;
6454 for (([start, end], paint), invisible) in
6455 invisible_iter.zip_eq(self.invisibles.iter())
6456 {
6457 let should_render = match (&last_seen, invisible) {
6458 (_, Invisible::Tab { .. }) => true,
6459 (Some((_, last_end, _)), _) => *last_end == start,
6460 _ => false,
6461 };
6462
6463 if should_render || start == 0 || end == self.len {
6464 paint(window, cx);
6465
6466 // Since we are scanning from the left, we will skip over the first available whitespace that is part
6467 // of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
6468 if let Some((should_render_last, last_end, paint_last)) = last_seen {
6469 // Note that we need to make sure that the last one is actually adjacent
6470 if !should_render_last && last_end == start {
6471 paint_last(window, cx);
6472 }
6473 }
6474 }
6475
6476 // Manually render anything within a selection
6477 let invisible_point = DisplayPoint::new(row, start as u32);
6478 if selection_ranges.iter().any(|region| {
6479 region.start <= invisible_point && invisible_point < region.end
6480 }) {
6481 paint(window, cx);
6482 }
6483
6484 last_seen = Some((should_render, end, paint));
6485 }
6486 }
6487 }
6488 }
6489
6490 pub fn x_for_index(&self, index: usize) -> Pixels {
6491 let mut fragment_start_x = Pixels::ZERO;
6492 let mut fragment_start_index = 0;
6493
6494 for fragment in &self.fragments {
6495 match fragment {
6496 LineFragment::Text(shaped_line) => {
6497 let fragment_end_index = fragment_start_index + shaped_line.len;
6498 if index < fragment_end_index {
6499 return fragment_start_x
6500 + shaped_line.x_for_index(index - fragment_start_index);
6501 }
6502 fragment_start_x += shaped_line.width;
6503 fragment_start_index = fragment_end_index;
6504 }
6505 LineFragment::Element { len, size, .. } => {
6506 let fragment_end_index = fragment_start_index + len;
6507 if index < fragment_end_index {
6508 return fragment_start_x;
6509 }
6510 fragment_start_x += size.width;
6511 fragment_start_index = fragment_end_index;
6512 }
6513 }
6514 }
6515
6516 fragment_start_x
6517 }
6518
6519 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
6520 let mut fragment_start_x = Pixels::ZERO;
6521 let mut fragment_start_index = 0;
6522
6523 for fragment in &self.fragments {
6524 match fragment {
6525 LineFragment::Text(shaped_line) => {
6526 let fragment_end_x = fragment_start_x + shaped_line.width;
6527 if x < fragment_end_x {
6528 return Some(
6529 fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
6530 );
6531 }
6532 fragment_start_x = fragment_end_x;
6533 fragment_start_index += shaped_line.len;
6534 }
6535 LineFragment::Element { len, size, .. } => {
6536 let fragment_end_x = fragment_start_x + size.width;
6537 if x < fragment_end_x {
6538 return Some(fragment_start_index);
6539 }
6540 fragment_start_index += len;
6541 fragment_start_x = fragment_end_x;
6542 }
6543 }
6544 }
6545
6546 None
6547 }
6548
6549 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
6550 let mut fragment_start_index = 0;
6551
6552 for fragment in &self.fragments {
6553 match fragment {
6554 LineFragment::Text(shaped_line) => {
6555 let fragment_end_index = fragment_start_index + shaped_line.len;
6556 if index < fragment_end_index {
6557 return shaped_line.font_id_for_index(index - fragment_start_index);
6558 }
6559 fragment_start_index = fragment_end_index;
6560 }
6561 LineFragment::Element { len, .. } => {
6562 let fragment_end_index = fragment_start_index + len;
6563 if index < fragment_end_index {
6564 return None;
6565 }
6566 fragment_start_index = fragment_end_index;
6567 }
6568 }
6569 }
6570
6571 None
6572 }
6573}
6574
6575#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6576enum Invisible {
6577 /// A tab character
6578 ///
6579 /// A tab character is internally represented by spaces (configured by the user's tab width)
6580 /// aligned to the nearest column, so it's necessary to store the start and end offset for
6581 /// adjacency checks.
6582 Tab {
6583 line_start_offset: usize,
6584 line_end_offset: usize,
6585 },
6586 Whitespace {
6587 line_offset: usize,
6588 },
6589}
6590
6591impl EditorElement {
6592 /// Returns the rem size to use when rendering the [`EditorElement`].
6593 ///
6594 /// This allows UI elements to scale based on the `buffer_font_size`.
6595 fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
6596 match self.editor.read(cx).mode {
6597 EditorMode::Full => {
6598 let buffer_font_size = self.style.text.font_size;
6599 match buffer_font_size {
6600 AbsoluteLength::Pixels(pixels) => {
6601 let rem_size_scale = {
6602 // Our default UI font size is 14px on a 16px base scale.
6603 // This means the default UI font size is 0.875rems.
6604 let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
6605
6606 // We then determine the delta between a single rem and the default font
6607 // size scale.
6608 let default_font_size_delta = 1. - default_font_size_scale;
6609
6610 // Finally, we add this delta to 1rem to get the scale factor that
6611 // should be used to scale up the UI.
6612 1. + default_font_size_delta
6613 };
6614
6615 Some(pixels * rem_size_scale)
6616 }
6617 AbsoluteLength::Rems(rems) => {
6618 Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into()))
6619 }
6620 }
6621 }
6622 // We currently use single-line and auto-height editors in UI contexts,
6623 // so we don't want to scale everything with the buffer font size, as it
6624 // ends up looking off.
6625 EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => None,
6626 }
6627 }
6628}
6629
6630impl Element for EditorElement {
6631 type RequestLayoutState = ();
6632 type PrepaintState = EditorLayout;
6633
6634 fn id(&self) -> Option<ElementId> {
6635 None
6636 }
6637
6638 fn request_layout(
6639 &mut self,
6640 _: Option<&GlobalElementId>,
6641 window: &mut Window,
6642 cx: &mut App,
6643 ) -> (gpui::LayoutId, ()) {
6644 let rem_size = self.rem_size(cx);
6645 window.with_rem_size(rem_size, |window| {
6646 self.editor.update(cx, |editor, cx| {
6647 editor.set_style(self.style.clone(), window, cx);
6648
6649 let layout_id = match editor.mode {
6650 EditorMode::SingleLine { auto_width } => {
6651 let rem_size = window.rem_size();
6652
6653 let height = self.style.text.line_height_in_pixels(rem_size);
6654 if auto_width {
6655 let editor_handle = cx.entity().clone();
6656 let style = self.style.clone();
6657 window.request_measured_layout(
6658 Style::default(),
6659 move |_, _, window, cx| {
6660 let editor_snapshot = editor_handle
6661 .update(cx, |editor, cx| editor.snapshot(window, cx));
6662 let line = Self::layout_lines(
6663 DisplayRow(0)..DisplayRow(1),
6664 &editor_snapshot,
6665 &style,
6666 px(f32::MAX),
6667 |_| false, // Single lines never soft wrap
6668 window,
6669 cx,
6670 )
6671 .pop()
6672 .unwrap();
6673
6674 let font_id =
6675 window.text_system().resolve_font(&style.text.font());
6676 let font_size =
6677 style.text.font_size.to_pixels(window.rem_size());
6678 let em_width =
6679 window.text_system().em_width(font_id, font_size).unwrap();
6680
6681 size(line.width + em_width, height)
6682 },
6683 )
6684 } else {
6685 let mut style = Style::default();
6686 style.size.height = height.into();
6687 style.size.width = relative(1.).into();
6688 window.request_layout(style, None, cx)
6689 }
6690 }
6691 EditorMode::AutoHeight { max_lines } => {
6692 let editor_handle = cx.entity().clone();
6693 let max_line_number_width =
6694 self.max_line_number_width(&editor.snapshot(window, cx), window, cx);
6695 window.request_measured_layout(
6696 Style::default(),
6697 move |known_dimensions, available_space, window, cx| {
6698 editor_handle
6699 .update(cx, |editor, cx| {
6700 compute_auto_height_layout(
6701 editor,
6702 max_lines,
6703 max_line_number_width,
6704 known_dimensions,
6705 available_space.width,
6706 window,
6707 cx,
6708 )
6709 })
6710 .unwrap_or_default()
6711 },
6712 )
6713 }
6714 EditorMode::Full => {
6715 let mut style = Style::default();
6716 style.size.width = relative(1.).into();
6717 style.size.height = relative(1.).into();
6718 window.request_layout(style, None, cx)
6719 }
6720 };
6721
6722 (layout_id, ())
6723 })
6724 })
6725 }
6726
6727 fn prepaint(
6728 &mut self,
6729 _: Option<&GlobalElementId>,
6730 bounds: Bounds<Pixels>,
6731 _: &mut Self::RequestLayoutState,
6732 window: &mut Window,
6733 cx: &mut App,
6734 ) -> Self::PrepaintState {
6735 let text_style = TextStyleRefinement {
6736 font_size: Some(self.style.text.font_size),
6737 line_height: Some(self.style.text.line_height),
6738 ..Default::default()
6739 };
6740 let focus_handle = self.editor.focus_handle(cx);
6741 window.set_view_id(self.editor.entity_id());
6742 window.set_focus_handle(&focus_handle, cx);
6743
6744 let rem_size = self.rem_size(cx);
6745 window.with_rem_size(rem_size, |window| {
6746 window.with_text_style(Some(text_style), |window| {
6747 window.with_content_mask(Some(ContentMask { bounds }), |window| {
6748 let mut snapshot = self
6749 .editor
6750 .update(cx, |editor, cx| editor.snapshot(window, cx));
6751 let style = self.style.clone();
6752
6753 let font_id = window.text_system().resolve_font(&style.text.font());
6754 let font_size = style.text.font_size.to_pixels(window.rem_size());
6755 let line_height = style.text.line_height_in_pixels(window.rem_size());
6756 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
6757 let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
6758
6759 let letter_size = size(em_width, line_height);
6760
6761 let gutter_dimensions = snapshot
6762 .gutter_dimensions(
6763 font_id,
6764 font_size,
6765 self.max_line_number_width(&snapshot, window, cx),
6766 cx,
6767 )
6768 .unwrap_or_default();
6769 let text_width = bounds.size.width - gutter_dimensions.width;
6770
6771 let editor_width =
6772 text_width - gutter_dimensions.margin - em_width - style.scrollbar_width;
6773
6774 snapshot = self.editor.update(cx, |editor, cx| {
6775 editor.last_bounds = Some(bounds);
6776 editor.gutter_dimensions = gutter_dimensions;
6777 editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
6778
6779 if matches!(editor.mode, EditorMode::AutoHeight { .. }) {
6780 snapshot
6781 } else {
6782 let wrap_width = match editor.soft_wrap_mode(cx) {
6783 SoftWrap::GitDiff => None,
6784 SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
6785 SoftWrap::EditorWidth => Some(editor_width),
6786 SoftWrap::Column(column) => Some(column as f32 * em_advance),
6787 SoftWrap::Bounded(column) => {
6788 Some(editor_width.min(column as f32 * em_advance))
6789 }
6790 };
6791
6792 if editor.set_wrap_width(wrap_width, cx) {
6793 editor.snapshot(window, cx)
6794 } else {
6795 snapshot
6796 }
6797 }
6798 });
6799
6800 let wrap_guides = self
6801 .editor
6802 .read(cx)
6803 .wrap_guides(cx)
6804 .iter()
6805 .map(|(guide, active)| (self.column_pixels(*guide, window, cx), *active))
6806 .collect::<SmallVec<[_; 2]>>();
6807
6808 let hitbox = window.insert_hitbox(bounds, false);
6809 let gutter_hitbox =
6810 window.insert_hitbox(gutter_bounds(bounds, gutter_dimensions), false);
6811 let text_hitbox = window.insert_hitbox(
6812 Bounds {
6813 origin: gutter_hitbox.top_right(),
6814 size: size(text_width, bounds.size.height),
6815 },
6816 false,
6817 );
6818 // Offset the content_bounds from the text_bounds by the gutter margin (which
6819 // is roughly half a character wide) to make hit testing work more like how we want.
6820 let content_origin =
6821 text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO);
6822
6823 let scrollbar_bounds =
6824 Bounds::from_corners(content_origin, bounds.bottom_right());
6825
6826 let height_in_lines = scrollbar_bounds.size.height / line_height;
6827
6828 // NOTE: The max row number in the current file, minus one
6829 let max_row = snapshot.max_point().row().as_f32();
6830
6831 // NOTE: The max scroll position for the top of the window
6832 let max_scroll_top = if matches!(snapshot.mode, EditorMode::AutoHeight { .. }) {
6833 (max_row - height_in_lines + 1.).max(0.)
6834 } else {
6835 let settings = EditorSettings::get_global(cx);
6836 match settings.scroll_beyond_last_line {
6837 ScrollBeyondLastLine::OnePage => max_row,
6838 ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.).max(0.),
6839 ScrollBeyondLastLine::VerticalScrollMargin => {
6840 (max_row - height_in_lines + 1. + settings.vertical_scroll_margin)
6841 .max(0.)
6842 }
6843 }
6844 };
6845
6846 // TODO: Autoscrolling for both axes
6847 let mut autoscroll_request = None;
6848 let mut autoscroll_containing_element = false;
6849 let mut autoscroll_horizontally = false;
6850 self.editor.update(cx, |editor, cx| {
6851 autoscroll_request = editor.autoscroll_request();
6852 autoscroll_containing_element =
6853 autoscroll_request.is_some() || editor.has_pending_selection();
6854 // TODO: Is this horizontal or vertical?!
6855 autoscroll_horizontally = editor.autoscroll_vertically(
6856 bounds,
6857 line_height,
6858 max_scroll_top,
6859 window,
6860 cx,
6861 );
6862 snapshot = editor.snapshot(window, cx);
6863 });
6864
6865 let mut scroll_position = snapshot.scroll_position();
6866 // The scroll position is a fractional point, the whole number of which represents
6867 // the top of the window in terms of display rows.
6868 let start_row = DisplayRow(scroll_position.y as u32);
6869 let max_row = snapshot.max_point().row();
6870 let end_row = cmp::min(
6871 (scroll_position.y + height_in_lines).ceil() as u32,
6872 max_row.next_row().0,
6873 );
6874 let end_row = DisplayRow(end_row);
6875
6876 let row_infos = snapshot
6877 .row_infos(start_row)
6878 .take((start_row..end_row).len())
6879 .collect::<Vec<RowInfo>>();
6880 let is_row_soft_wrapped = |row: usize| {
6881 row_infos
6882 .get(row)
6883 .map_or(true, |info| info.buffer_row.is_none())
6884 };
6885
6886 let start_anchor = if start_row == Default::default() {
6887 Anchor::min()
6888 } else {
6889 snapshot.buffer_snapshot.anchor_before(
6890 DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
6891 )
6892 };
6893 let end_anchor = if end_row > max_row {
6894 Anchor::max()
6895 } else {
6896 snapshot.buffer_snapshot.anchor_before(
6897 DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
6898 )
6899 };
6900
6901 let mut highlighted_rows = self
6902 .editor
6903 .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
6904
6905 for (ix, row_info) in row_infos.iter().enumerate() {
6906 let Some(diff_status) = row_info.diff_status else {
6907 continue;
6908 };
6909
6910 let staged_opacity = 0.10;
6911 let unstaged_opacity = 0.04;
6912
6913 let background_color = match diff_status.kind {
6914 DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
6915 DiffHunkStatusKind::Deleted => {
6916 cx.theme().colors().version_control_deleted
6917 }
6918 DiffHunkStatusKind::Modified => {
6919 debug_panic!("modified diff status for row info");
6920 continue;
6921 }
6922 };
6923 let background_color =
6924 if diff_status.secondary == DiffHunkSecondaryStatus::None {
6925 background_color.opacity(staged_opacity)
6926 } else {
6927 background_color.opacity(unstaged_opacity)
6928 };
6929
6930 highlighted_rows
6931 .entry(start_row + DisplayRow(ix as u32))
6932 .or_insert(background_color.into());
6933 }
6934
6935 let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
6936 start_anchor..end_anchor,
6937 &snapshot.display_snapshot,
6938 cx.theme().colors(),
6939 );
6940 let highlighted_gutter_ranges =
6941 self.editor.read(cx).gutter_highlights_in_range(
6942 start_anchor..end_anchor,
6943 &snapshot.display_snapshot,
6944 cx,
6945 );
6946
6947 let redacted_ranges = self.editor.read(cx).redacted_ranges(
6948 start_anchor..end_anchor,
6949 &snapshot.display_snapshot,
6950 cx,
6951 );
6952
6953 let (local_selections, selected_buffer_ids): (
6954 Vec<Selection<Point>>,
6955 Vec<BufferId>,
6956 ) = self.editor.update(cx, |editor, cx| {
6957 let all_selections = editor.selections.all::<Point>(cx);
6958 let selected_buffer_ids = if editor.is_singleton(cx) {
6959 Vec::new()
6960 } else {
6961 let mut selected_buffer_ids = Vec::with_capacity(all_selections.len());
6962
6963 for selection in all_selections {
6964 for buffer_id in snapshot
6965 .buffer_snapshot
6966 .buffer_ids_for_range(selection.range())
6967 {
6968 if selected_buffer_ids.last() != Some(&buffer_id) {
6969 selected_buffer_ids.push(buffer_id);
6970 }
6971 }
6972 }
6973
6974 selected_buffer_ids
6975 };
6976
6977 let mut selections = editor
6978 .selections
6979 .disjoint_in_range(start_anchor..end_anchor, cx);
6980 selections.extend(editor.selections.pending(cx));
6981
6982 (selections, selected_buffer_ids)
6983 });
6984
6985 let (selections, active_rows, newest_selection_head) = self.layout_selections(
6986 start_anchor,
6987 end_anchor,
6988 &local_selections,
6989 &snapshot,
6990 start_row,
6991 end_row,
6992 window,
6993 cx,
6994 );
6995
6996 let line_numbers = self.layout_line_numbers(
6997 Some(&gutter_hitbox),
6998 gutter_dimensions,
6999 line_height,
7000 scroll_position,
7001 start_row..end_row,
7002 &row_infos,
7003 newest_selection_head,
7004 &snapshot,
7005 window,
7006 cx,
7007 );
7008
7009 let mut crease_toggles =
7010 window.with_element_namespace("crease_toggles", |window| {
7011 self.layout_crease_toggles(
7012 start_row..end_row,
7013 &row_infos,
7014 &active_rows,
7015 &snapshot,
7016 window,
7017 cx,
7018 )
7019 });
7020 let crease_trailers =
7021 window.with_element_namespace("crease_trailers", |window| {
7022 self.layout_crease_trailers(
7023 row_infos.iter().copied(),
7024 &snapshot,
7025 window,
7026 cx,
7027 )
7028 });
7029
7030 let display_hunks = self.layout_gutter_diff_hunks(
7031 line_height,
7032 &gutter_hitbox,
7033 start_row..end_row,
7034 &snapshot,
7035 window,
7036 cx,
7037 );
7038
7039 let mut line_layouts = Self::layout_lines(
7040 start_row..end_row,
7041 &snapshot,
7042 &self.style,
7043 editor_width,
7044 is_row_soft_wrapped,
7045 window,
7046 cx,
7047 );
7048
7049 let longest_line_blame_width = self
7050 .editor
7051 .update(cx, |editor, cx| {
7052 if !editor.show_git_blame_inline {
7053 return None;
7054 }
7055 let blame = editor.blame.as_ref()?;
7056 let blame_entry = blame
7057 .update(cx, |blame, cx| {
7058 let row_infos =
7059 snapshot.row_infos(snapshot.longest_row()).next()?;
7060 blame.blame_for_rows(&[row_infos], cx).next()
7061 })
7062 .flatten()?;
7063 let mut element = render_inline_blame_entry(
7064 self.editor.clone(),
7065 blame,
7066 blame_entry,
7067 &style,
7068 cx,
7069 );
7070 let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance;
7071 Some(
7072 element
7073 .layout_as_root(AvailableSpace::min_size(), window, cx)
7074 .width
7075 + inline_blame_padding,
7076 )
7077 })
7078 .unwrap_or(Pixels::ZERO);
7079
7080 let longest_line_width = layout_line(
7081 snapshot.longest_row(),
7082 &snapshot,
7083 &style,
7084 editor_width,
7085 is_row_soft_wrapped,
7086 window,
7087 cx,
7088 )
7089 .width;
7090
7091 let scrollbar_range_data = ScrollbarRangeData::new(
7092 scrollbar_bounds,
7093 letter_size,
7094 &snapshot,
7095 longest_line_width,
7096 longest_line_blame_width,
7097 &style,
7098 editor_width,
7099 cx,
7100 );
7101
7102 let scroll_range_bounds = scrollbar_range_data.scroll_range;
7103 let mut scroll_width = scroll_range_bounds.size.width;
7104
7105 let sticky_header_excerpt = if snapshot.buffer_snapshot.show_headers() {
7106 snapshot.sticky_header_excerpt(start_row)
7107 } else {
7108 None
7109 };
7110 let sticky_header_excerpt_id =
7111 sticky_header_excerpt.as_ref().map(|top| top.excerpt.id);
7112
7113 let blocks = window.with_element_namespace("blocks", |window| {
7114 self.render_blocks(
7115 start_row..end_row,
7116 &snapshot,
7117 &hitbox,
7118 &text_hitbox,
7119 editor_width,
7120 &mut scroll_width,
7121 &gutter_dimensions,
7122 em_width,
7123 gutter_dimensions.full_width(),
7124 line_height,
7125 &line_layouts,
7126 &local_selections,
7127 &selected_buffer_ids,
7128 is_row_soft_wrapped,
7129 sticky_header_excerpt_id,
7130 window,
7131 cx,
7132 )
7133 });
7134 let mut blocks = match blocks {
7135 Ok(blocks) => blocks,
7136 Err(resized_blocks) => {
7137 self.editor.update(cx, |editor, cx| {
7138 editor.resize_blocks(resized_blocks, autoscroll_request, cx)
7139 });
7140 return self.prepaint(None, bounds, &mut (), window, cx);
7141 }
7142 };
7143
7144 let sticky_buffer_header = sticky_header_excerpt.map(|sticky_header_excerpt| {
7145 window.with_element_namespace("blocks", |window| {
7146 self.layout_sticky_buffer_header(
7147 sticky_header_excerpt,
7148 scroll_position.y,
7149 line_height,
7150 &snapshot,
7151 &hitbox,
7152 &selected_buffer_ids,
7153 window,
7154 cx,
7155 )
7156 })
7157 });
7158
7159 let start_buffer_row =
7160 MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
7161 let end_buffer_row =
7162 MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
7163
7164 let scroll_max = point(
7165 ((scroll_width - scrollbar_bounds.size.width) / em_width).max(0.0),
7166 max_row.as_f32(),
7167 );
7168
7169 self.editor.update(cx, |editor, cx| {
7170 let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
7171
7172 let autoscrolled = if autoscroll_horizontally {
7173 editor.autoscroll_horizontally(
7174 start_row,
7175 editor_width - (letter_size.width / 2.0) + style.scrollbar_width,
7176 scroll_width,
7177 em_width,
7178 &line_layouts,
7179 cx,
7180 )
7181 } else {
7182 false
7183 };
7184
7185 if clamped || autoscrolled {
7186 snapshot = editor.snapshot(window, cx);
7187 scroll_position = snapshot.scroll_position();
7188 }
7189 });
7190
7191 let scroll_pixel_position = point(
7192 scroll_position.x * em_width,
7193 scroll_position.y * line_height,
7194 );
7195
7196 let indent_guides = self.layout_indent_guides(
7197 content_origin,
7198 text_hitbox.origin,
7199 start_buffer_row..end_buffer_row,
7200 scroll_pixel_position,
7201 line_height,
7202 &snapshot,
7203 window,
7204 cx,
7205 );
7206
7207 let crease_trailers =
7208 window.with_element_namespace("crease_trailers", |window| {
7209 self.prepaint_crease_trailers(
7210 crease_trailers,
7211 &line_layouts,
7212 line_height,
7213 content_origin,
7214 scroll_pixel_position,
7215 em_width,
7216 window,
7217 cx,
7218 )
7219 });
7220
7221 let mut inline_blame = None;
7222 if let Some(newest_selection_head) = newest_selection_head {
7223 let display_row = newest_selection_head.row();
7224 if (start_row..end_row).contains(&display_row) {
7225 let line_ix = display_row.minus(start_row) as usize;
7226 let row_info = &row_infos[line_ix];
7227 let line_layout = &line_layouts[line_ix];
7228 let crease_trailer_layout = crease_trailers[line_ix].as_ref();
7229 inline_blame = self.layout_inline_blame(
7230 display_row,
7231 row_info,
7232 line_layout,
7233 crease_trailer_layout,
7234 em_width,
7235 content_origin,
7236 scroll_pixel_position,
7237 line_height,
7238 window,
7239 cx,
7240 );
7241 }
7242 }
7243
7244 let blamed_display_rows = self.layout_blame_entries(
7245 &row_infos,
7246 em_width,
7247 scroll_position,
7248 line_height,
7249 &gutter_hitbox,
7250 gutter_dimensions.git_blame_entries_width,
7251 window,
7252 cx,
7253 );
7254
7255 let scroll_max = point(
7256 ((scroll_width - scrollbar_bounds.size.width) / em_width).max(0.0),
7257 max_scroll_top,
7258 );
7259
7260 self.editor.update(cx, |editor, cx| {
7261 let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
7262
7263 let autoscrolled = if autoscroll_horizontally {
7264 editor.autoscroll_horizontally(
7265 start_row,
7266 editor_width - (letter_size.width / 2.0) + style.scrollbar_width,
7267 scroll_width,
7268 em_width,
7269 &line_layouts,
7270 cx,
7271 )
7272 } else {
7273 false
7274 };
7275
7276 if clamped || autoscrolled {
7277 snapshot = editor.snapshot(window, cx);
7278 scroll_position = snapshot.scroll_position();
7279 }
7280 });
7281
7282 let line_elements = self.prepaint_lines(
7283 start_row,
7284 &mut line_layouts,
7285 line_height,
7286 scroll_pixel_position,
7287 content_origin,
7288 window,
7289 cx,
7290 );
7291
7292 let mut block_start_rows = HashSet::default();
7293
7294 window.with_element_namespace("blocks", |window| {
7295 self.layout_blocks(
7296 &mut blocks,
7297 &mut block_start_rows,
7298 &hitbox,
7299 line_height,
7300 scroll_pixel_position,
7301 window,
7302 cx,
7303 );
7304 });
7305
7306 let cursors = self.collect_cursors(&snapshot, cx);
7307 let visible_row_range = start_row..end_row;
7308 let non_visible_cursors = cursors
7309 .iter()
7310 .any(|c| !visible_row_range.contains(&c.0.row()));
7311
7312 let visible_cursors = self.layout_visible_cursors(
7313 &snapshot,
7314 &selections,
7315 &block_start_rows,
7316 start_row..end_row,
7317 &line_layouts,
7318 &text_hitbox,
7319 content_origin,
7320 scroll_position,
7321 scroll_pixel_position,
7322 line_height,
7323 em_width,
7324 em_advance,
7325 autoscroll_containing_element,
7326 window,
7327 cx,
7328 );
7329
7330 let scrollbars_layout = self.layout_scrollbars(
7331 &snapshot,
7332 scrollbar_range_data,
7333 scroll_position,
7334 non_visible_cursors,
7335 window,
7336 cx,
7337 );
7338
7339 let gutter_settings = EditorSettings::get_global(cx).gutter;
7340
7341 let rows_with_hunk_bounds = display_hunks
7342 .iter()
7343 .filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds)))
7344 .fold(
7345 HashMap::default(),
7346 |mut rows_with_hunk_bounds, (hunk, bounds)| {
7347 match hunk {
7348 DisplayDiffHunk::Folded { display_row } => {
7349 rows_with_hunk_bounds.insert(*display_row, bounds);
7350 }
7351 DisplayDiffHunk::Unfolded {
7352 display_row_range, ..
7353 } => {
7354 for display_row in display_row_range.iter_rows() {
7355 rows_with_hunk_bounds.insert(display_row, bounds);
7356 }
7357 }
7358 }
7359 rows_with_hunk_bounds
7360 },
7361 );
7362 let mut code_actions_indicator = None;
7363 if let Some(newest_selection_head) = newest_selection_head {
7364 let newest_selection_point =
7365 newest_selection_head.to_point(&snapshot.display_snapshot);
7366
7367 if (start_row..end_row).contains(&newest_selection_head.row()) {
7368 self.layout_cursor_popovers(
7369 line_height,
7370 &text_hitbox,
7371 content_origin,
7372 start_row,
7373 scroll_pixel_position,
7374 &line_layouts,
7375 newest_selection_head,
7376 newest_selection_point,
7377 &style,
7378 window,
7379 cx,
7380 );
7381
7382 let show_code_actions = snapshot
7383 .show_code_actions
7384 .unwrap_or(gutter_settings.code_actions);
7385 if show_code_actions {
7386 let newest_selection_point =
7387 newest_selection_head.to_point(&snapshot.display_snapshot);
7388 if !snapshot
7389 .is_line_folded(MultiBufferRow(newest_selection_point.row))
7390 {
7391 let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
7392 MultiBufferRow(newest_selection_point.row),
7393 );
7394 if let Some((buffer, range)) = buffer {
7395 let buffer_id = buffer.remote_id();
7396 let row = range.start.row;
7397 let has_test_indicator = self
7398 .editor
7399 .read(cx)
7400 .tasks
7401 .contains_key(&(buffer_id, row));
7402
7403 if !has_test_indicator {
7404 code_actions_indicator = self
7405 .layout_code_actions_indicator(
7406 line_height,
7407 newest_selection_head,
7408 scroll_pixel_position,
7409 &gutter_dimensions,
7410 &gutter_hitbox,
7411 &rows_with_hunk_bounds,
7412 window,
7413 cx,
7414 );
7415 }
7416 }
7417 }
7418 }
7419 }
7420 }
7421
7422 self.layout_gutter_menu(
7423 line_height,
7424 &text_hitbox,
7425 content_origin,
7426 scroll_pixel_position,
7427 gutter_dimensions.width - gutter_dimensions.left_padding,
7428 window,
7429 cx,
7430 );
7431
7432 let test_indicators = if gutter_settings.runnables {
7433 self.layout_run_indicators(
7434 line_height,
7435 start_row..end_row,
7436 scroll_pixel_position,
7437 &gutter_dimensions,
7438 &gutter_hitbox,
7439 &rows_with_hunk_bounds,
7440 &snapshot,
7441 window,
7442 cx,
7443 )
7444 } else {
7445 Vec::new()
7446 };
7447
7448 self.layout_signature_help(
7449 &hitbox,
7450 content_origin,
7451 scroll_pixel_position,
7452 newest_selection_head,
7453 start_row,
7454 &line_layouts,
7455 line_height,
7456 em_width,
7457 window,
7458 cx,
7459 );
7460
7461 if !cx.has_active_drag() {
7462 self.layout_hover_popovers(
7463 &snapshot,
7464 &hitbox,
7465 &text_hitbox,
7466 start_row..end_row,
7467 content_origin,
7468 scroll_pixel_position,
7469 &line_layouts,
7470 line_height,
7471 em_width,
7472 window,
7473 cx,
7474 );
7475 }
7476
7477 let inline_completion_popover = self.layout_edit_prediction_popover(
7478 &text_hitbox.bounds,
7479 content_origin,
7480 &snapshot,
7481 start_row..end_row,
7482 scroll_position.y,
7483 scroll_position.y + height_in_lines,
7484 &line_layouts,
7485 line_height,
7486 scroll_pixel_position,
7487 newest_selection_head,
7488 editor_width,
7489 &style,
7490 window,
7491 cx,
7492 );
7493
7494 let mouse_context_menu = self.layout_mouse_context_menu(
7495 &snapshot,
7496 start_row..end_row,
7497 content_origin,
7498 window,
7499 cx,
7500 );
7501
7502 window.with_element_namespace("crease_toggles", |window| {
7503 self.prepaint_crease_toggles(
7504 &mut crease_toggles,
7505 line_height,
7506 &gutter_dimensions,
7507 gutter_settings,
7508 scroll_pixel_position,
7509 &gutter_hitbox,
7510 window,
7511 cx,
7512 )
7513 });
7514
7515 let invisible_symbol_font_size = font_size / 2.;
7516 let tab_invisible = window
7517 .text_system()
7518 .shape_line(
7519 "→".into(),
7520 invisible_symbol_font_size,
7521 &[TextRun {
7522 len: "→".len(),
7523 font: self.style.text.font(),
7524 color: cx.theme().colors().editor_invisible,
7525 background_color: None,
7526 underline: None,
7527 strikethrough: None,
7528 }],
7529 )
7530 .unwrap();
7531 let space_invisible = window
7532 .text_system()
7533 .shape_line(
7534 "•".into(),
7535 invisible_symbol_font_size,
7536 &[TextRun {
7537 len: "•".len(),
7538 font: self.style.text.font(),
7539 color: cx.theme().colors().editor_invisible,
7540 background_color: None,
7541 underline: None,
7542 strikethrough: None,
7543 }],
7544 )
7545 .unwrap();
7546
7547 let mode = snapshot.mode;
7548
7549 let position_map = Rc::new(PositionMap {
7550 size: bounds.size,
7551 visible_row_range,
7552 scroll_pixel_position,
7553 scroll_max,
7554 line_layouts,
7555 line_height,
7556 em_width,
7557 em_advance,
7558 snapshot,
7559 gutter_hitbox: gutter_hitbox.clone(),
7560 text_hitbox: text_hitbox.clone(),
7561 });
7562
7563 self.editor.update(cx, |editor, _| {
7564 editor.last_position_map = Some(position_map.clone())
7565 });
7566
7567 let hunk_controls = self.layout_diff_hunk_controls(
7568 start_row..end_row,
7569 &row_infos,
7570 &text_hitbox,
7571 &position_map,
7572 newest_selection_head,
7573 line_height,
7574 scroll_pixel_position,
7575 &display_hunks,
7576 self.editor.clone(),
7577 window,
7578 cx,
7579 );
7580
7581 EditorLayout {
7582 mode,
7583 position_map,
7584 visible_display_row_range: start_row..end_row,
7585 wrap_guides,
7586 indent_guides,
7587 hitbox,
7588 gutter_hitbox,
7589 display_hunks,
7590 content_origin,
7591 scrollbars_layout,
7592 active_rows,
7593 highlighted_rows,
7594 highlighted_ranges,
7595 highlighted_gutter_ranges,
7596 redacted_ranges,
7597 line_elements,
7598 line_numbers,
7599 blamed_display_rows,
7600 inline_blame,
7601 blocks,
7602 cursors,
7603 visible_cursors,
7604 selections,
7605 inline_completion_popover,
7606 diff_hunk_controls: hunk_controls,
7607 mouse_context_menu,
7608 test_indicators,
7609 code_actions_indicator,
7610 crease_toggles,
7611 crease_trailers,
7612 tab_invisible,
7613 space_invisible,
7614 sticky_buffer_header,
7615 }
7616 })
7617 })
7618 })
7619 }
7620
7621 fn paint(
7622 &mut self,
7623 _: Option<&GlobalElementId>,
7624 bounds: Bounds<gpui::Pixels>,
7625 _: &mut Self::RequestLayoutState,
7626 layout: &mut Self::PrepaintState,
7627 window: &mut Window,
7628 cx: &mut App,
7629 ) {
7630 let focus_handle = self.editor.focus_handle(cx);
7631 let key_context = self
7632 .editor
7633 .update(cx, |editor, cx| editor.key_context(window, cx));
7634
7635 window.set_key_context(key_context);
7636 window.handle_input(
7637 &focus_handle,
7638 ElementInputHandler::new(bounds, self.editor.clone()),
7639 cx,
7640 );
7641 self.register_actions(window, cx);
7642 self.register_key_listeners(window, cx, layout);
7643
7644 let text_style = TextStyleRefinement {
7645 font_size: Some(self.style.text.font_size),
7646 line_height: Some(self.style.text.line_height),
7647 ..Default::default()
7648 };
7649 let rem_size = self.rem_size(cx);
7650 window.with_rem_size(rem_size, |window| {
7651 window.with_text_style(Some(text_style), |window| {
7652 window.with_content_mask(Some(ContentMask { bounds }), |window| {
7653 self.paint_mouse_listeners(layout, window, cx);
7654 self.paint_background(layout, window, cx);
7655 self.paint_indent_guides(layout, window, cx);
7656
7657 if layout.gutter_hitbox.size.width > Pixels::ZERO {
7658 self.paint_blamed_display_rows(layout, window, cx);
7659 self.paint_line_numbers(layout, window, cx);
7660 }
7661
7662 self.paint_text(layout, window, cx);
7663
7664 if layout.gutter_hitbox.size.width > Pixels::ZERO {
7665 self.paint_gutter_highlights(layout, window, cx);
7666 self.paint_gutter_indicators(layout, window, cx);
7667 }
7668
7669 if !layout.blocks.is_empty() {
7670 window.with_element_namespace("blocks", |window| {
7671 self.paint_blocks(layout, window, cx);
7672 });
7673 }
7674
7675 window.with_element_namespace("blocks", |window| {
7676 if let Some(mut sticky_header) = layout.sticky_buffer_header.take() {
7677 sticky_header.paint(window, cx)
7678 }
7679 });
7680
7681 self.paint_scrollbars(layout, window, cx);
7682 self.paint_inline_completion_popover(layout, window, cx);
7683 self.paint_mouse_context_menu(layout, window, cx);
7684 });
7685 })
7686 })
7687 }
7688}
7689
7690pub(super) fn gutter_bounds(
7691 editor_bounds: Bounds<Pixels>,
7692 gutter_dimensions: GutterDimensions,
7693) -> Bounds<Pixels> {
7694 Bounds {
7695 origin: editor_bounds.origin,
7696 size: size(gutter_dimensions.width, editor_bounds.size.height),
7697 }
7698}
7699
7700struct ScrollbarRangeData {
7701 scrollbar_bounds: Bounds<Pixels>,
7702 scroll_range: Bounds<Pixels>,
7703 letter_size: Size<Pixels>,
7704}
7705
7706impl ScrollbarRangeData {
7707 #[allow(clippy::too_many_arguments)]
7708 pub fn new(
7709 scrollbar_bounds: Bounds<Pixels>,
7710 letter_size: Size<Pixels>,
7711 snapshot: &EditorSnapshot,
7712 longest_line_width: Pixels,
7713 longest_line_blame_width: Pixels,
7714 style: &EditorStyle,
7715 editor_width: Pixels,
7716 cx: &mut App,
7717 ) -> ScrollbarRangeData {
7718 // TODO: Simplify this function down, it requires a lot of parameters
7719 let max_row = snapshot.max_point().row();
7720 let text_bounds_size = size(longest_line_width, max_row.0 as f32 * letter_size.height);
7721
7722 let settings = EditorSettings::get_global(cx);
7723 let scroll_beyond_last_line: Pixels = match settings.scroll_beyond_last_line {
7724 ScrollBeyondLastLine::OnePage => px(scrollbar_bounds.size.height / letter_size.height),
7725 ScrollBeyondLastLine::Off => px(1.),
7726 ScrollBeyondLastLine::VerticalScrollMargin => px(1.0 + settings.vertical_scroll_margin),
7727 };
7728
7729 let right_margin = if longest_line_width + longest_line_blame_width >= editor_width {
7730 letter_size.width + style.scrollbar_width
7731 } else {
7732 px(0.0)
7733 };
7734
7735 let overscroll = size(
7736 right_margin + longest_line_blame_width,
7737 letter_size.height * scroll_beyond_last_line,
7738 );
7739
7740 let scroll_range = Bounds {
7741 origin: scrollbar_bounds.origin,
7742 size: text_bounds_size + overscroll,
7743 };
7744
7745 ScrollbarRangeData {
7746 scrollbar_bounds,
7747 scroll_range,
7748 letter_size,
7749 }
7750 }
7751}
7752
7753impl IntoElement for EditorElement {
7754 type Element = Self;
7755
7756 fn into_element(self) -> Self::Element {
7757 self
7758 }
7759}
7760
7761pub struct EditorLayout {
7762 position_map: Rc<PositionMap>,
7763 hitbox: Hitbox,
7764 gutter_hitbox: Hitbox,
7765 content_origin: gpui::Point<Pixels>,
7766 scrollbars_layout: AxisPair<Option<ScrollbarLayout>>,
7767 mode: EditorMode,
7768 wrap_guides: SmallVec<[(Pixels, bool); 2]>,
7769 indent_guides: Option<Vec<IndentGuideLayout>>,
7770 visible_display_row_range: Range<DisplayRow>,
7771 active_rows: BTreeMap<DisplayRow, bool>,
7772 highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
7773 line_elements: SmallVec<[AnyElement; 1]>,
7774 line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
7775 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
7776 blamed_display_rows: Option<Vec<AnyElement>>,
7777 inline_blame: Option<AnyElement>,
7778 blocks: Vec<BlockLayout>,
7779 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
7780 highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
7781 redacted_ranges: Vec<Range<DisplayPoint>>,
7782 cursors: Vec<(DisplayPoint, Hsla)>,
7783 visible_cursors: Vec<CursorLayout>,
7784 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
7785 code_actions_indicator: Option<AnyElement>,
7786 test_indicators: Vec<AnyElement>,
7787 crease_toggles: Vec<Option<AnyElement>>,
7788 diff_hunk_controls: Vec<AnyElement>,
7789 crease_trailers: Vec<Option<CreaseTrailerLayout>>,
7790 inline_completion_popover: Option<AnyElement>,
7791 mouse_context_menu: Option<AnyElement>,
7792 tab_invisible: ShapedLine,
7793 space_invisible: ShapedLine,
7794 sticky_buffer_header: Option<AnyElement>,
7795}
7796
7797impl EditorLayout {
7798 fn line_end_overshoot(&self) -> Pixels {
7799 0.15 * self.position_map.line_height
7800 }
7801}
7802
7803struct LineNumberLayout {
7804 shaped_line: ShapedLine,
7805 hitbox: Option<Hitbox>,
7806 display_row: DisplayRow,
7807}
7808
7809struct ColoredRange<T> {
7810 start: T,
7811 end: T,
7812 color: Hsla,
7813}
7814
7815#[derive(Clone)]
7816struct ScrollbarLayout {
7817 hitbox: Hitbox,
7818 visible_range: Range<f32>,
7819 visible: bool,
7820 text_unit_size: Pixels,
7821 thumb_size: Pixels,
7822 axis: Axis,
7823}
7824
7825impl ScrollbarLayout {
7826 const BORDER_WIDTH: Pixels = px(1.0);
7827 const LINE_MARKER_HEIGHT: Pixels = px(2.0);
7828 const MIN_MARKER_HEIGHT: Pixels = px(5.0);
7829 // const MIN_THUMB_HEIGHT: Pixels = px(20.0);
7830
7831 fn thumb_bounds(&self) -> Bounds<Pixels> {
7832 match self.axis {
7833 Axis::Vertical => {
7834 let thumb_top = self.y_for_row(self.visible_range.start);
7835 let thumb_bottom = thumb_top + self.thumb_size;
7836 Bounds::from_corners(
7837 point(self.hitbox.left(), thumb_top),
7838 point(self.hitbox.right(), thumb_bottom),
7839 )
7840 }
7841 Axis::Horizontal => {
7842 let thumb_left =
7843 self.hitbox.left() + self.visible_range.start * self.text_unit_size;
7844 let thumb_right = thumb_left + self.thumb_size;
7845 Bounds::from_corners(
7846 point(thumb_left, self.hitbox.top()),
7847 point(thumb_right, self.hitbox.bottom()),
7848 )
7849 }
7850 }
7851 }
7852
7853 fn y_for_row(&self, row: f32) -> Pixels {
7854 self.hitbox.top() + row * self.text_unit_size
7855 }
7856
7857 fn marker_quads_for_ranges(
7858 &self,
7859 row_ranges: impl IntoIterator<Item = ColoredRange<DisplayRow>>,
7860 column: Option<usize>,
7861 ) -> Vec<PaintQuad> {
7862 struct MinMax {
7863 min: Pixels,
7864 max: Pixels,
7865 }
7866 let (x_range, height_limit) = if let Some(column) = column {
7867 let column_width = px(((self.hitbox.size.width - Self::BORDER_WIDTH).0 / 3.0).floor());
7868 let start = Self::BORDER_WIDTH + (column as f32 * column_width);
7869 let end = start + column_width;
7870 (
7871 Range { start, end },
7872 MinMax {
7873 min: Self::MIN_MARKER_HEIGHT,
7874 max: px(f32::MAX),
7875 },
7876 )
7877 } else {
7878 (
7879 Range {
7880 start: Self::BORDER_WIDTH,
7881 end: self.hitbox.size.width,
7882 },
7883 MinMax {
7884 min: Self::LINE_MARKER_HEIGHT,
7885 max: Self::LINE_MARKER_HEIGHT,
7886 },
7887 )
7888 };
7889
7890 let row_to_y = |row: DisplayRow| row.as_f32() * self.text_unit_size;
7891 let mut pixel_ranges = row_ranges
7892 .into_iter()
7893 .map(|range| {
7894 let start_y = row_to_y(range.start);
7895 let end_y = row_to_y(range.end)
7896 + self
7897 .text_unit_size
7898 .max(height_limit.min)
7899 .min(height_limit.max);
7900 ColoredRange {
7901 start: start_y,
7902 end: end_y,
7903 color: range.color,
7904 }
7905 })
7906 .peekable();
7907
7908 let mut quads = Vec::new();
7909 while let Some(mut pixel_range) = pixel_ranges.next() {
7910 while let Some(next_pixel_range) = pixel_ranges.peek() {
7911 if pixel_range.end >= next_pixel_range.start - px(1.0)
7912 && pixel_range.color == next_pixel_range.color
7913 {
7914 pixel_range.end = next_pixel_range.end.max(pixel_range.end);
7915 pixel_ranges.next();
7916 } else {
7917 break;
7918 }
7919 }
7920
7921 let bounds = Bounds::from_corners(
7922 point(x_range.start, pixel_range.start),
7923 point(x_range.end, pixel_range.end),
7924 );
7925 quads.push(quad(
7926 bounds,
7927 Corners::default(),
7928 pixel_range.color,
7929 Edges::default(),
7930 Hsla::transparent_black(),
7931 ));
7932 }
7933
7934 quads
7935 }
7936}
7937
7938struct CreaseTrailerLayout {
7939 element: AnyElement,
7940 bounds: Bounds<Pixels>,
7941}
7942
7943pub(crate) struct PositionMap {
7944 pub size: Size<Pixels>,
7945 pub line_height: Pixels,
7946 pub scroll_pixel_position: gpui::Point<Pixels>,
7947 pub scroll_max: gpui::Point<f32>,
7948 pub em_width: Pixels,
7949 pub em_advance: Pixels,
7950 pub visible_row_range: Range<DisplayRow>,
7951 pub line_layouts: Vec<LineWithInvisibles>,
7952 pub snapshot: EditorSnapshot,
7953 pub text_hitbox: Hitbox,
7954 pub gutter_hitbox: Hitbox,
7955}
7956
7957#[derive(Debug, Copy, Clone)]
7958pub struct PointForPosition {
7959 pub previous_valid: DisplayPoint,
7960 pub next_valid: DisplayPoint,
7961 pub exact_unclipped: DisplayPoint,
7962 pub column_overshoot_after_line_end: u32,
7963}
7964
7965impl PointForPosition {
7966 pub fn as_valid(&self) -> Option<DisplayPoint> {
7967 if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
7968 Some(self.previous_valid)
7969 } else {
7970 None
7971 }
7972 }
7973}
7974
7975impl PositionMap {
7976 pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
7977 let text_bounds = self.text_hitbox.bounds;
7978 let scroll_position = self.snapshot.scroll_position();
7979 let position = position - text_bounds.origin;
7980 let y = position.y.max(px(0.)).min(self.size.height);
7981 let x = position.x + (scroll_position.x * self.em_width);
7982 let row = ((y / self.line_height) + scroll_position.y) as u32;
7983
7984 let (column, x_overshoot_after_line_end) = if let Some(line) = self
7985 .line_layouts
7986 .get(row as usize - scroll_position.y as usize)
7987 {
7988 if let Some(ix) = line.index_for_x(x) {
7989 (ix as u32, px(0.))
7990 } else {
7991 (line.len as u32, px(0.).max(x - line.width))
7992 }
7993 } else {
7994 (0, x)
7995 };
7996
7997 let mut exact_unclipped = DisplayPoint::new(DisplayRow(row), column);
7998 let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
7999 let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
8000
8001 let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
8002 *exact_unclipped.column_mut() += column_overshoot_after_line_end;
8003 PointForPosition {
8004 previous_valid,
8005 next_valid,
8006 exact_unclipped,
8007 column_overshoot_after_line_end,
8008 }
8009 }
8010}
8011
8012struct BlockLayout {
8013 id: BlockId,
8014 row: Option<DisplayRow>,
8015 element: AnyElement,
8016 available_space: Size<AvailableSpace>,
8017 style: BlockStyle,
8018}
8019
8020fn layout_line(
8021 row: DisplayRow,
8022 snapshot: &EditorSnapshot,
8023 style: &EditorStyle,
8024 text_width: Pixels,
8025 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
8026 window: &mut Window,
8027 cx: &mut App,
8028) -> LineWithInvisibles {
8029 let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
8030 LineWithInvisibles::from_chunks(
8031 chunks,
8032 &style,
8033 MAX_LINE_LEN,
8034 1,
8035 snapshot.mode,
8036 text_width,
8037 is_row_soft_wrapped,
8038 window,
8039 cx,
8040 )
8041 .pop()
8042 .unwrap()
8043}
8044
8045#[derive(Debug)]
8046pub struct IndentGuideLayout {
8047 origin: gpui::Point<Pixels>,
8048 length: Pixels,
8049 single_indent_width: Pixels,
8050 depth: u32,
8051 active: bool,
8052 settings: IndentGuideSettings,
8053}
8054
8055pub struct CursorLayout {
8056 origin: gpui::Point<Pixels>,
8057 block_width: Pixels,
8058 line_height: Pixels,
8059 color: Hsla,
8060 shape: CursorShape,
8061 block_text: Option<ShapedLine>,
8062 cursor_name: Option<AnyElement>,
8063}
8064
8065#[derive(Debug)]
8066pub struct CursorName {
8067 string: SharedString,
8068 color: Hsla,
8069 is_top_row: bool,
8070}
8071
8072impl CursorLayout {
8073 pub fn new(
8074 origin: gpui::Point<Pixels>,
8075 block_width: Pixels,
8076 line_height: Pixels,
8077 color: Hsla,
8078 shape: CursorShape,
8079 block_text: Option<ShapedLine>,
8080 ) -> CursorLayout {
8081 CursorLayout {
8082 origin,
8083 block_width,
8084 line_height,
8085 color,
8086 shape,
8087 block_text,
8088 cursor_name: None,
8089 }
8090 }
8091
8092 pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
8093 Bounds {
8094 origin: self.origin + origin,
8095 size: size(self.block_width, self.line_height),
8096 }
8097 }
8098
8099 fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
8100 match self.shape {
8101 CursorShape::Bar => Bounds {
8102 origin: self.origin + origin,
8103 size: size(px(2.0), self.line_height),
8104 },
8105 CursorShape::Block | CursorShape::Hollow => Bounds {
8106 origin: self.origin + origin,
8107 size: size(self.block_width, self.line_height),
8108 },
8109 CursorShape::Underline => Bounds {
8110 origin: self.origin
8111 + origin
8112 + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)),
8113 size: size(self.block_width, px(2.0)),
8114 },
8115 }
8116 }
8117
8118 pub fn layout(
8119 &mut self,
8120 origin: gpui::Point<Pixels>,
8121 cursor_name: Option<CursorName>,
8122 window: &mut Window,
8123 cx: &mut App,
8124 ) {
8125 if let Some(cursor_name) = cursor_name {
8126 let bounds = self.bounds(origin);
8127 let text_size = self.line_height / 1.5;
8128
8129 let name_origin = if cursor_name.is_top_row {
8130 point(bounds.right() - px(1.), bounds.top())
8131 } else {
8132 match self.shape {
8133 CursorShape::Bar => point(
8134 bounds.right() - px(2.),
8135 bounds.top() - text_size / 2. - px(1.),
8136 ),
8137 _ => point(
8138 bounds.right() - px(1.),
8139 bounds.top() - text_size / 2. - px(1.),
8140 ),
8141 }
8142 };
8143 let mut name_element = div()
8144 .bg(self.color)
8145 .text_size(text_size)
8146 .px_0p5()
8147 .line_height(text_size + px(2.))
8148 .text_color(cursor_name.color)
8149 .child(cursor_name.string.clone())
8150 .into_any_element();
8151
8152 name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx);
8153
8154 self.cursor_name = Some(name_element);
8155 }
8156 }
8157
8158 pub fn paint(&mut self, origin: gpui::Point<Pixels>, window: &mut Window, cx: &mut App) {
8159 let bounds = self.bounds(origin);
8160
8161 //Draw background or border quad
8162 let cursor = if matches!(self.shape, CursorShape::Hollow) {
8163 outline(bounds, self.color)
8164 } else {
8165 fill(bounds, self.color)
8166 };
8167
8168 if let Some(name) = &mut self.cursor_name {
8169 name.paint(window, cx);
8170 }
8171
8172 window.paint_quad(cursor);
8173
8174 if let Some(block_text) = &self.block_text {
8175 block_text
8176 .paint(self.origin + origin, self.line_height, window, cx)
8177 .log_err();
8178 }
8179 }
8180
8181 pub fn shape(&self) -> CursorShape {
8182 self.shape
8183 }
8184}
8185
8186#[derive(Debug)]
8187pub struct HighlightedRange {
8188 pub start_y: Pixels,
8189 pub line_height: Pixels,
8190 pub lines: Vec<HighlightedRangeLine>,
8191 pub color: Hsla,
8192 pub corner_radius: Pixels,
8193}
8194
8195#[derive(Debug)]
8196pub struct HighlightedRangeLine {
8197 pub start_x: Pixels,
8198 pub end_x: Pixels,
8199}
8200
8201impl HighlightedRange {
8202 pub fn paint(&self, bounds: Bounds<Pixels>, window: &mut Window) {
8203 if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
8204 self.paint_lines(self.start_y, &self.lines[0..1], bounds, window);
8205 self.paint_lines(
8206 self.start_y + self.line_height,
8207 &self.lines[1..],
8208 bounds,
8209 window,
8210 );
8211 } else {
8212 self.paint_lines(self.start_y, &self.lines, bounds, window);
8213 }
8214 }
8215
8216 fn paint_lines(
8217 &self,
8218 start_y: Pixels,
8219 lines: &[HighlightedRangeLine],
8220 _bounds: Bounds<Pixels>,
8221 window: &mut Window,
8222 ) {
8223 if lines.is_empty() {
8224 return;
8225 }
8226
8227 let first_line = lines.first().unwrap();
8228 let last_line = lines.last().unwrap();
8229
8230 let first_top_left = point(first_line.start_x, start_y);
8231 let first_top_right = point(first_line.end_x, start_y);
8232
8233 let curve_height = point(Pixels::ZERO, self.corner_radius);
8234 let curve_width = |start_x: Pixels, end_x: Pixels| {
8235 let max = (end_x - start_x) / 2.;
8236 let width = if max < self.corner_radius {
8237 max
8238 } else {
8239 self.corner_radius
8240 };
8241
8242 point(width, Pixels::ZERO)
8243 };
8244
8245 let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
8246 let mut builder = gpui::PathBuilder::fill();
8247 builder.move_to(first_top_right - top_curve_width);
8248 builder.curve_to(first_top_right + curve_height, first_top_right);
8249
8250 let mut iter = lines.iter().enumerate().peekable();
8251 while let Some((ix, line)) = iter.next() {
8252 let bottom_right = point(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
8253
8254 if let Some((_, next_line)) = iter.peek() {
8255 let next_top_right = point(next_line.end_x, bottom_right.y);
8256
8257 match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() {
8258 Ordering::Equal => {
8259 builder.line_to(bottom_right);
8260 }
8261 Ordering::Less => {
8262 let curve_width = curve_width(next_top_right.x, bottom_right.x);
8263 builder.line_to(bottom_right - curve_height);
8264 if self.corner_radius > Pixels::ZERO {
8265 builder.curve_to(bottom_right - curve_width, bottom_right);
8266 }
8267 builder.line_to(next_top_right + curve_width);
8268 if self.corner_radius > Pixels::ZERO {
8269 builder.curve_to(next_top_right + curve_height, next_top_right);
8270 }
8271 }
8272 Ordering::Greater => {
8273 let curve_width = curve_width(bottom_right.x, next_top_right.x);
8274 builder.line_to(bottom_right - curve_height);
8275 if self.corner_radius > Pixels::ZERO {
8276 builder.curve_to(bottom_right + curve_width, bottom_right);
8277 }
8278 builder.line_to(next_top_right - curve_width);
8279 if self.corner_radius > Pixels::ZERO {
8280 builder.curve_to(next_top_right + curve_height, next_top_right);
8281 }
8282 }
8283 }
8284 } else {
8285 let curve_width = curve_width(line.start_x, line.end_x);
8286 builder.line_to(bottom_right - curve_height);
8287 if self.corner_radius > Pixels::ZERO {
8288 builder.curve_to(bottom_right - curve_width, bottom_right);
8289 }
8290
8291 let bottom_left = point(line.start_x, bottom_right.y);
8292 builder.line_to(bottom_left + curve_width);
8293 if self.corner_radius > Pixels::ZERO {
8294 builder.curve_to(bottom_left - curve_height, bottom_left);
8295 }
8296 }
8297 }
8298
8299 if first_line.start_x > last_line.start_x {
8300 let curve_width = curve_width(last_line.start_x, first_line.start_x);
8301 let second_top_left = point(last_line.start_x, start_y + self.line_height);
8302 builder.line_to(second_top_left + curve_height);
8303 if self.corner_radius > Pixels::ZERO {
8304 builder.curve_to(second_top_left + curve_width, second_top_left);
8305 }
8306 let first_bottom_left = point(first_line.start_x, second_top_left.y);
8307 builder.line_to(first_bottom_left - curve_width);
8308 if self.corner_radius > Pixels::ZERO {
8309 builder.curve_to(first_bottom_left - curve_height, first_bottom_left);
8310 }
8311 }
8312
8313 builder.line_to(first_top_left + curve_height);
8314 if self.corner_radius > Pixels::ZERO {
8315 builder.curve_to(first_top_left + top_curve_width, first_top_left);
8316 }
8317 builder.line_to(first_top_right - top_curve_width);
8318
8319 if let Ok(path) = builder.build() {
8320 window.paint_path(path, self.color);
8321 }
8322 }
8323}
8324
8325enum CursorPopoverType {
8326 CodeContextMenu,
8327 EditPrediction,
8328}
8329
8330pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
8331 (delta.pow(1.5) / 100.0).into()
8332}
8333
8334fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
8335 (delta.pow(1.2) / 300.0).into()
8336}
8337
8338pub fn register_action<T: Action>(
8339 editor: &Entity<Editor>,
8340 window: &mut Window,
8341 listener: impl Fn(&mut Editor, &T, &mut Window, &mut Context<Editor>) + 'static,
8342) {
8343 let editor = editor.clone();
8344 window.on_action(TypeId::of::<T>(), move |action, phase, window, cx| {
8345 let action = action.downcast_ref().unwrap();
8346 if phase == DispatchPhase::Bubble {
8347 editor.update(cx, |editor, cx| {
8348 listener(editor, action, window, cx);
8349 })
8350 }
8351 })
8352}
8353
8354fn compute_auto_height_layout(
8355 editor: &mut Editor,
8356 max_lines: usize,
8357 max_line_number_width: Pixels,
8358 known_dimensions: Size<Option<Pixels>>,
8359 available_width: AvailableSpace,
8360 window: &mut Window,
8361 cx: &mut Context<Editor>,
8362) -> Option<Size<Pixels>> {
8363 let width = known_dimensions.width.or({
8364 if let AvailableSpace::Definite(available_width) = available_width {
8365 Some(available_width)
8366 } else {
8367 None
8368 }
8369 })?;
8370 if let Some(height) = known_dimensions.height {
8371 return Some(size(width, height));
8372 }
8373
8374 let style = editor.style.as_ref().unwrap();
8375 let font_id = window.text_system().resolve_font(&style.text.font());
8376 let font_size = style.text.font_size.to_pixels(window.rem_size());
8377 let line_height = style.text.line_height_in_pixels(window.rem_size());
8378 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
8379
8380 let mut snapshot = editor.snapshot(window, cx);
8381 let gutter_dimensions = snapshot
8382 .gutter_dimensions(font_id, font_size, max_line_number_width, cx)
8383 .unwrap_or_default();
8384
8385 editor.gutter_dimensions = gutter_dimensions;
8386 let text_width = width - gutter_dimensions.width;
8387 let overscroll = size(em_width, px(0.));
8388
8389 let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
8390 if editor.set_wrap_width(Some(editor_width), cx) {
8391 snapshot = editor.snapshot(window, cx);
8392 }
8393
8394 let scroll_height = Pixels::from(snapshot.max_point().row().next_row().0) * line_height;
8395 let height = scroll_height
8396 .max(line_height)
8397 .min(line_height * max_lines as f32);
8398
8399 Some(size(width, height))
8400}
8401
8402#[cfg(test)]
8403mod tests {
8404 use super::*;
8405 use crate::{
8406 display_map::{BlockPlacement, BlockProperties},
8407 editor_tests::{init_test, update_test_language_settings},
8408 Editor, MultiBuffer,
8409 };
8410 use gpui::{TestAppContext, VisualTestContext};
8411 use language::language_settings;
8412 use log::info;
8413 use std::num::NonZeroU32;
8414 use util::test::sample_text;
8415
8416 #[gpui::test]
8417 fn test_shape_line_numbers(cx: &mut TestAppContext) {
8418 init_test(cx, |_| {});
8419 let window = cx.add_window(|window, cx| {
8420 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
8421 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
8422 });
8423
8424 let editor = window.root(cx).unwrap();
8425 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
8426 let line_height = window
8427 .update(cx, |_, window, _| {
8428 style.text.line_height_in_pixels(window.rem_size())
8429 })
8430 .unwrap();
8431 let element = EditorElement::new(&editor, style);
8432 let snapshot = window
8433 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
8434 .unwrap();
8435
8436 let layouts = cx
8437 .update_window(*window, |_, window, cx| {
8438 element.layout_line_numbers(
8439 None,
8440 GutterDimensions {
8441 left_padding: Pixels::ZERO,
8442 right_padding: Pixels::ZERO,
8443 width: px(30.0),
8444 margin: Pixels::ZERO,
8445 git_blame_entries_width: None,
8446 },
8447 line_height,
8448 gpui::Point::default(),
8449 DisplayRow(0)..DisplayRow(6),
8450 &(0..6)
8451 .map(|row| RowInfo {
8452 buffer_row: Some(row),
8453 ..Default::default()
8454 })
8455 .collect::<Vec<_>>(),
8456 Some(DisplayPoint::new(DisplayRow(0), 0)),
8457 &snapshot,
8458 window,
8459 cx,
8460 )
8461 })
8462 .unwrap();
8463 assert_eq!(layouts.len(), 6);
8464
8465 let relative_rows = window
8466 .update(cx, |editor, window, cx| {
8467 let snapshot = editor.snapshot(window, cx);
8468 element.calculate_relative_line_numbers(
8469 &snapshot,
8470 &(DisplayRow(0)..DisplayRow(6)),
8471 Some(DisplayRow(3)),
8472 )
8473 })
8474 .unwrap();
8475 assert_eq!(relative_rows[&DisplayRow(0)], 3);
8476 assert_eq!(relative_rows[&DisplayRow(1)], 2);
8477 assert_eq!(relative_rows[&DisplayRow(2)], 1);
8478 // current line has no relative number
8479 assert_eq!(relative_rows[&DisplayRow(4)], 1);
8480 assert_eq!(relative_rows[&DisplayRow(5)], 2);
8481
8482 // works if cursor is before screen
8483 let relative_rows = window
8484 .update(cx, |editor, window, cx| {
8485 let snapshot = editor.snapshot(window, cx);
8486 element.calculate_relative_line_numbers(
8487 &snapshot,
8488 &(DisplayRow(3)..DisplayRow(6)),
8489 Some(DisplayRow(1)),
8490 )
8491 })
8492 .unwrap();
8493 assert_eq!(relative_rows.len(), 3);
8494 assert_eq!(relative_rows[&DisplayRow(3)], 2);
8495 assert_eq!(relative_rows[&DisplayRow(4)], 3);
8496 assert_eq!(relative_rows[&DisplayRow(5)], 4);
8497
8498 // works if cursor is after screen
8499 let relative_rows = window
8500 .update(cx, |editor, window, cx| {
8501 let snapshot = editor.snapshot(window, cx);
8502 element.calculate_relative_line_numbers(
8503 &snapshot,
8504 &(DisplayRow(0)..DisplayRow(3)),
8505 Some(DisplayRow(6)),
8506 )
8507 })
8508 .unwrap();
8509 assert_eq!(relative_rows.len(), 3);
8510 assert_eq!(relative_rows[&DisplayRow(0)], 5);
8511 assert_eq!(relative_rows[&DisplayRow(1)], 4);
8512 assert_eq!(relative_rows[&DisplayRow(2)], 3);
8513 }
8514
8515 #[gpui::test]
8516 async fn test_vim_visual_selections(cx: &mut TestAppContext) {
8517 init_test(cx, |_| {});
8518
8519 let window = cx.add_window(|window, cx| {
8520 let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
8521 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
8522 });
8523 let cx = &mut VisualTestContext::from_window(*window, cx);
8524 let editor = window.root(cx).unwrap();
8525 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
8526
8527 window
8528 .update(cx, |editor, window, cx| {
8529 editor.cursor_shape = CursorShape::Block;
8530 editor.change_selections(None, window, cx, |s| {
8531 s.select_ranges([
8532 Point::new(0, 0)..Point::new(1, 0),
8533 Point::new(3, 2)..Point::new(3, 3),
8534 Point::new(5, 6)..Point::new(6, 0),
8535 ]);
8536 });
8537 })
8538 .unwrap();
8539
8540 let (_, state) = cx.draw(
8541 point(px(500.), px(500.)),
8542 size(px(500.), px(500.)),
8543 |_, _| EditorElement::new(&editor, style),
8544 );
8545
8546 assert_eq!(state.selections.len(), 1);
8547 let local_selections = &state.selections[0].1;
8548 assert_eq!(local_selections.len(), 3);
8549 // moves cursor back one line
8550 assert_eq!(
8551 local_selections[0].head,
8552 DisplayPoint::new(DisplayRow(0), 6)
8553 );
8554 assert_eq!(
8555 local_selections[0].range,
8556 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0)
8557 );
8558
8559 // moves cursor back one column
8560 assert_eq!(
8561 local_selections[1].range,
8562 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 3)
8563 );
8564 assert_eq!(
8565 local_selections[1].head,
8566 DisplayPoint::new(DisplayRow(3), 2)
8567 );
8568
8569 // leaves cursor on the max point
8570 assert_eq!(
8571 local_selections[2].range,
8572 DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(6), 0)
8573 );
8574 assert_eq!(
8575 local_selections[2].head,
8576 DisplayPoint::new(DisplayRow(6), 0)
8577 );
8578
8579 // active lines does not include 1 (even though the range of the selection does)
8580 assert_eq!(
8581 state.active_rows.keys().cloned().collect::<Vec<_>>(),
8582 vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
8583 );
8584
8585 // multi-buffer support
8586 // in DisplayPoint coordinates, this is what we're dealing with:
8587 // 0: [[file
8588 // 1: header
8589 // 2: section]]
8590 // 3: aaaaaa
8591 // 4: bbbbbb
8592 // 5: cccccc
8593 // 6:
8594 // 7: [[footer]]
8595 // 8: [[header]]
8596 // 9: ffffff
8597 // 10: gggggg
8598 // 11: hhhhhh
8599 // 12:
8600 // 13: [[footer]]
8601 // 14: [[file
8602 // 15: header
8603 // 16: section]]
8604 // 17: bbbbbb
8605 // 18: cccccc
8606 // 19: dddddd
8607 // 20: [[footer]]
8608 let window = cx.add_window(|window, cx| {
8609 let buffer = MultiBuffer::build_multi(
8610 [
8611 (
8612 &(sample_text(8, 6, 'a') + "\n"),
8613 vec![
8614 Point::new(0, 0)..Point::new(3, 0),
8615 Point::new(4, 0)..Point::new(7, 0),
8616 ],
8617 ),
8618 (
8619 &(sample_text(8, 6, 'a') + "\n"),
8620 vec![Point::new(1, 0)..Point::new(3, 0)],
8621 ),
8622 ],
8623 cx,
8624 );
8625 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
8626 });
8627 let editor = window.root(cx).unwrap();
8628 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
8629 let _state = window.update(cx, |editor, window, cx| {
8630 editor.cursor_shape = CursorShape::Block;
8631 editor.change_selections(None, window, cx, |s| {
8632 s.select_display_ranges([
8633 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0),
8634 DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0),
8635 ]);
8636 });
8637 });
8638
8639 let (_, state) = cx.draw(
8640 point(px(500.), px(500.)),
8641 size(px(500.), px(500.)),
8642 |_, _| EditorElement::new(&editor, style),
8643 );
8644 assert_eq!(state.selections.len(), 1);
8645 let local_selections = &state.selections[0].1;
8646 assert_eq!(local_selections.len(), 2);
8647
8648 // moves cursor on excerpt boundary back a line
8649 // and doesn't allow selection to bleed through
8650 assert_eq!(
8651 local_selections[0].range,
8652 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0)
8653 );
8654 assert_eq!(
8655 local_selections[0].head,
8656 DisplayPoint::new(DisplayRow(6), 0)
8657 );
8658 // moves cursor on buffer boundary back two lines
8659 // and doesn't allow selection to bleed through
8660 assert_eq!(
8661 local_selections[1].range,
8662 DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0)
8663 );
8664 assert_eq!(
8665 local_selections[1].head,
8666 DisplayPoint::new(DisplayRow(12), 0)
8667 );
8668 }
8669
8670 #[gpui::test]
8671 fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
8672 init_test(cx, |_| {});
8673
8674 let window = cx.add_window(|window, cx| {
8675 let buffer = MultiBuffer::build_simple("", cx);
8676 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
8677 });
8678 let cx = &mut VisualTestContext::from_window(*window, cx);
8679 let editor = window.root(cx).unwrap();
8680 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
8681 window
8682 .update(cx, |editor, window, cx| {
8683 editor.set_placeholder_text("hello", cx);
8684 editor.insert_blocks(
8685 [BlockProperties {
8686 style: BlockStyle::Fixed,
8687 placement: BlockPlacement::Above(Anchor::min()),
8688 height: 3,
8689 render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
8690 priority: 0,
8691 }],
8692 None,
8693 cx,
8694 );
8695
8696 // Blur the editor so that it displays placeholder text.
8697 window.blur();
8698 })
8699 .unwrap();
8700
8701 let (_, state) = cx.draw(
8702 point(px(500.), px(500.)),
8703 size(px(500.), px(500.)),
8704 |_, _| EditorElement::new(&editor, style),
8705 );
8706 assert_eq!(state.position_map.line_layouts.len(), 4);
8707 assert_eq!(state.line_numbers.len(), 1);
8708 assert_eq!(
8709 state
8710 .line_numbers
8711 .get(&MultiBufferRow(0))
8712 .map(|line_number| line_number.shaped_line.text.as_ref()),
8713 Some("1")
8714 );
8715 }
8716
8717 #[gpui::test]
8718 fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
8719 const TAB_SIZE: u32 = 4;
8720
8721 let input_text = "\t \t|\t| a b";
8722 let expected_invisibles = vec![
8723 Invisible::Tab {
8724 line_start_offset: 0,
8725 line_end_offset: TAB_SIZE as usize,
8726 },
8727 Invisible::Whitespace {
8728 line_offset: TAB_SIZE as usize,
8729 },
8730 Invisible::Tab {
8731 line_start_offset: TAB_SIZE as usize + 1,
8732 line_end_offset: TAB_SIZE as usize * 2,
8733 },
8734 Invisible::Tab {
8735 line_start_offset: TAB_SIZE as usize * 2 + 1,
8736 line_end_offset: TAB_SIZE as usize * 3,
8737 },
8738 Invisible::Whitespace {
8739 line_offset: TAB_SIZE as usize * 3 + 1,
8740 },
8741 Invisible::Whitespace {
8742 line_offset: TAB_SIZE as usize * 3 + 3,
8743 },
8744 ];
8745 assert_eq!(
8746 expected_invisibles.len(),
8747 input_text
8748 .chars()
8749 .filter(|initial_char| initial_char.is_whitespace())
8750 .count(),
8751 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
8752 );
8753
8754 for show_line_numbers in [true, false] {
8755 init_test(cx, |s| {
8756 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
8757 s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
8758 });
8759
8760 let actual_invisibles = collect_invisibles_from_new_editor(
8761 cx,
8762 EditorMode::Full,
8763 input_text,
8764 px(500.0),
8765 show_line_numbers,
8766 );
8767
8768 assert_eq!(expected_invisibles, actual_invisibles);
8769 }
8770 }
8771
8772 #[gpui::test]
8773 fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
8774 init_test(cx, |s| {
8775 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
8776 s.defaults.tab_size = NonZeroU32::new(4);
8777 });
8778
8779 for editor_mode_without_invisibles in [
8780 EditorMode::SingleLine { auto_width: false },
8781 EditorMode::AutoHeight { max_lines: 100 },
8782 ] {
8783 for show_line_numbers in [true, false] {
8784 let invisibles = collect_invisibles_from_new_editor(
8785 cx,
8786 editor_mode_without_invisibles,
8787 "\t\t\t| | a b",
8788 px(500.0),
8789 show_line_numbers,
8790 );
8791 assert!(invisibles.is_empty(),
8792 "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
8793 }
8794 }
8795 }
8796
8797 #[gpui::test]
8798 fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
8799 let tab_size = 4;
8800 let input_text = "a\tbcd ".repeat(9);
8801 let repeated_invisibles = [
8802 Invisible::Tab {
8803 line_start_offset: 1,
8804 line_end_offset: tab_size as usize,
8805 },
8806 Invisible::Whitespace {
8807 line_offset: tab_size as usize + 3,
8808 },
8809 Invisible::Whitespace {
8810 line_offset: tab_size as usize + 4,
8811 },
8812 Invisible::Whitespace {
8813 line_offset: tab_size as usize + 5,
8814 },
8815 Invisible::Whitespace {
8816 line_offset: tab_size as usize + 6,
8817 },
8818 Invisible::Whitespace {
8819 line_offset: tab_size as usize + 7,
8820 },
8821 ];
8822 let expected_invisibles = std::iter::once(repeated_invisibles)
8823 .cycle()
8824 .take(9)
8825 .flatten()
8826 .collect::<Vec<_>>();
8827 assert_eq!(
8828 expected_invisibles.len(),
8829 input_text
8830 .chars()
8831 .filter(|initial_char| initial_char.is_whitespace())
8832 .count(),
8833 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
8834 );
8835 info!("Expected invisibles: {expected_invisibles:?}");
8836
8837 init_test(cx, |_| {});
8838
8839 // Put the same string with repeating whitespace pattern into editors of various size,
8840 // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
8841 let resize_step = 10.0;
8842 let mut editor_width = 200.0;
8843 while editor_width <= 1000.0 {
8844 for show_line_numbers in [true, false] {
8845 update_test_language_settings(cx, |s| {
8846 s.defaults.tab_size = NonZeroU32::new(tab_size);
8847 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
8848 s.defaults.preferred_line_length = Some(editor_width as u32);
8849 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
8850 });
8851
8852 let actual_invisibles = collect_invisibles_from_new_editor(
8853 cx,
8854 EditorMode::Full,
8855 &input_text,
8856 px(editor_width),
8857 show_line_numbers,
8858 );
8859
8860 // Whatever the editor size is, ensure it has the same invisible kinds in the same order
8861 // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
8862 let mut i = 0;
8863 for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
8864 i = actual_index;
8865 match expected_invisibles.get(i) {
8866 Some(expected_invisible) => match (expected_invisible, actual_invisible) {
8867 (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
8868 | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
8869 _ => {
8870 panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
8871 }
8872 },
8873 None => {
8874 panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
8875 }
8876 }
8877 }
8878 let missing_expected_invisibles = &expected_invisibles[i + 1..];
8879 assert!(
8880 missing_expected_invisibles.is_empty(),
8881 "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
8882 );
8883
8884 editor_width += resize_step;
8885 }
8886 }
8887 }
8888
8889 fn collect_invisibles_from_new_editor(
8890 cx: &mut TestAppContext,
8891 editor_mode: EditorMode,
8892 input_text: &str,
8893 editor_width: Pixels,
8894 show_line_numbers: bool,
8895 ) -> Vec<Invisible> {
8896 info!(
8897 "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
8898 editor_width.0
8899 );
8900 let window = cx.add_window(|window, cx| {
8901 let buffer = MultiBuffer::build_simple(input_text, cx);
8902 Editor::new(editor_mode, buffer, None, true, window, cx)
8903 });
8904 let cx = &mut VisualTestContext::from_window(*window, cx);
8905 let editor = window.root(cx).unwrap();
8906
8907 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
8908 window
8909 .update(cx, |editor, _, cx| {
8910 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
8911 editor.set_wrap_width(Some(editor_width), cx);
8912 editor.set_show_line_numbers(show_line_numbers, cx);
8913 })
8914 .unwrap();
8915 let (_, state) = cx.draw(
8916 point(px(500.), px(500.)),
8917 size(px(500.), px(500.)),
8918 |_, _| EditorElement::new(&editor, style),
8919 );
8920 state
8921 .position_map
8922 .line_layouts
8923 .iter()
8924 .flat_map(|line_with_invisibles| &line_with_invisibles.invisibles)
8925 .cloned()
8926 .collect()
8927 }
8928}
8929
8930fn diff_hunk_controls(
8931 row: u32,
8932 hunk_range: Range<Anchor>,
8933 line_height: Pixels,
8934 editor: &Entity<Editor>,
8935 cx: &mut App,
8936) -> AnyElement {
8937 h_flex()
8938 .h(line_height)
8939 .mr_1()
8940 .gap_1()
8941 .px_1()
8942 .pb_1()
8943 .border_b_1()
8944 .border_color(cx.theme().colors().border_variant)
8945 .rounded_b_lg()
8946 .bg(cx.theme().colors().editor_background)
8947 .gap_1()
8948 .child(
8949 IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
8950 .shape(IconButtonShape::Square)
8951 .icon_size(IconSize::Small)
8952 // .disabled(!has_multiple_hunks)
8953 .tooltip({
8954 let focus_handle = editor.focus_handle(cx);
8955 move |window, cx| {
8956 Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, window, cx)
8957 }
8958 })
8959 .on_click({
8960 let editor = editor.clone();
8961 move |_event, window, cx| {
8962 editor.update(cx, |editor, cx| {
8963 let snapshot = editor.snapshot(window, cx);
8964 let position = hunk_range.end.to_point(&snapshot.buffer_snapshot);
8965 editor.go_to_hunk_after_position(&snapshot, position, window, cx);
8966 editor.expand_selected_diff_hunks(cx);
8967 });
8968 }
8969 }),
8970 )
8971 .child(
8972 IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
8973 .shape(IconButtonShape::Square)
8974 .icon_size(IconSize::Small)
8975 // .disabled(!has_multiple_hunks)
8976 .tooltip({
8977 let focus_handle = editor.focus_handle(cx);
8978 move |window, cx| {
8979 Tooltip::for_action_in(
8980 "Previous Hunk",
8981 &GoToPrevHunk,
8982 &focus_handle,
8983 window,
8984 cx,
8985 )
8986 }
8987 })
8988 .on_click({
8989 let editor = editor.clone();
8990 move |_event, window, cx| {
8991 editor.update(cx, |editor, cx| {
8992 let snapshot = editor.snapshot(window, cx);
8993 let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
8994 editor.go_to_hunk_before_position(&snapshot, point, window, cx);
8995 editor.expand_selected_diff_hunks(cx);
8996 });
8997 }
8998 }),
8999 )
9000 .child(
9001 IconButton::new("discard", IconName::Undo)
9002 .shape(IconButtonShape::Square)
9003 .icon_size(IconSize::Small)
9004 .tooltip({
9005 let focus_handle = editor.focus_handle(cx);
9006 move |window, cx| {
9007 Tooltip::for_action_in(
9008 "Restore Hunk",
9009 &::git::Restore,
9010 &focus_handle,
9011 window,
9012 cx,
9013 )
9014 }
9015 })
9016 .on_click({
9017 let editor = editor.clone();
9018 move |_event, window, cx| {
9019 editor.update(cx, |editor, cx| {
9020 let snapshot = editor.snapshot(window, cx);
9021 let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
9022 editor.restore_hunks_in_ranges(vec![point..point], window, cx);
9023 });
9024 }
9025 }),
9026 )
9027 .into_any_element()
9028}