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