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