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