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