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