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