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