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