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