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 let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
8700 let blame = blame.read(cx);
8701 let repository = blame.repository(cx, buffer)?;
8702 renderer.render_blame_entry_popover(
8703 blame_entry,
8704 scroll_handle,
8705 commit_message,
8706 markdown,
8707 repository,
8708 workspace,
8709 window,
8710 cx,
8711 )
8712}
8713
8714fn render_blame_entry(
8715 ix: usize,
8716 blame: &Entity<GitBlame>,
8717 blame_entry: BlameEntry,
8718 style: &EditorStyle,
8719 last_used_color: &mut Option<(Hsla, Oid)>,
8720 editor: Entity<Editor>,
8721 workspace: Entity<Workspace>,
8722 buffer: BufferId,
8723 renderer: &dyn BlameRenderer,
8724 window: &mut Window,
8725 cx: &mut App,
8726) -> Option<AnyElement> {
8727 let index: u32 = blame_entry.sha.into();
8728 let mut sha_color = cx.theme().players().color_for_participant(index).cursor;
8729
8730 // If the last color we used is the same as the one we get for this line, but
8731 // the commit SHAs are different, then we try again to get a different color.
8732 if let Some((color, sha)) = *last_used_color
8733 && sha != blame_entry.sha
8734 && color == sha_color
8735 {
8736 sha_color = cx.theme().players().color_for_participant(index + 1).cursor;
8737 }
8738 last_used_color.replace((sha_color, blame_entry.sha));
8739
8740 let blame = blame.read(cx);
8741 let details = blame.details_for_entry(buffer, &blame_entry);
8742 let repository = blame.repository(cx, buffer)?;
8743 renderer.render_blame_entry(
8744 &style.text,
8745 blame_entry,
8746 details,
8747 repository,
8748 workspace.downgrade(),
8749 editor,
8750 ix,
8751 sha_color,
8752 window,
8753 cx,
8754 )
8755}
8756
8757#[derive(Debug)]
8758pub(crate) struct LineWithInvisibles {
8759 fragments: SmallVec<[LineFragment; 1]>,
8760 invisibles: Vec<Invisible>,
8761 len: usize,
8762 pub(crate) width: Pixels,
8763 font_size: Pixels,
8764}
8765
8766enum LineFragment {
8767 Text(ShapedLine),
8768 Element {
8769 id: ChunkRendererId,
8770 element: Option<AnyElement>,
8771 size: Size<Pixels>,
8772 len: usize,
8773 },
8774}
8775
8776impl fmt::Debug for LineFragment {
8777 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
8778 match self {
8779 LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
8780 LineFragment::Element { size, len, .. } => f
8781 .debug_struct("Element")
8782 .field("size", size)
8783 .field("len", len)
8784 .finish(),
8785 }
8786 }
8787}
8788
8789impl LineWithInvisibles {
8790 fn from_chunks<'a>(
8791 chunks: impl Iterator<Item = HighlightedChunk<'a>>,
8792 editor_style: &EditorStyle,
8793 max_line_len: usize,
8794 max_line_count: usize,
8795 editor_mode: &EditorMode,
8796 text_width: Pixels,
8797 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
8798 bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
8799 window: &mut Window,
8800 cx: &mut App,
8801 ) -> Vec<Self> {
8802 let text_style = &editor_style.text;
8803 let mut layouts = Vec::with_capacity(max_line_count);
8804 let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
8805 let mut line = String::new();
8806 let mut invisibles = Vec::new();
8807 let mut width = Pixels::ZERO;
8808 let mut len = 0;
8809 let mut styles = Vec::new();
8810 let mut non_whitespace_added = false;
8811 let mut row = 0;
8812 let mut line_exceeded_max_len = false;
8813 let font_size = text_style.font_size.to_pixels(window.rem_size());
8814 let min_contrast = EditorSettings::get_global(cx).minimum_contrast_for_highlights;
8815
8816 let ellipsis = SharedString::from("β―");
8817
8818 for highlighted_chunk in chunks.chain([HighlightedChunk {
8819 text: "\n",
8820 style: None,
8821 is_tab: false,
8822 is_inlay: false,
8823 replacement: None,
8824 }]) {
8825 if let Some(replacement) = highlighted_chunk.replacement {
8826 if !line.is_empty() {
8827 let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
8828 let text_runs: &[TextRun] = if segments.is_empty() {
8829 &styles
8830 } else {
8831 &Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
8832 };
8833 let shaped_line = window.text_system().shape_line(
8834 line.clone().into(),
8835 font_size,
8836 text_runs,
8837 None,
8838 );
8839 width += shaped_line.width;
8840 len += shaped_line.len;
8841 fragments.push(LineFragment::Text(shaped_line));
8842 line.clear();
8843 styles.clear();
8844 }
8845
8846 match replacement {
8847 ChunkReplacement::Renderer(renderer) => {
8848 let available_width = if renderer.constrain_width {
8849 let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
8850 ellipsis.clone()
8851 } else {
8852 SharedString::from(Arc::from(highlighted_chunk.text))
8853 };
8854 let shaped_line = window.text_system().shape_line(
8855 chunk,
8856 font_size,
8857 &[text_style.to_run(highlighted_chunk.text.len())],
8858 None,
8859 );
8860 AvailableSpace::Definite(shaped_line.width)
8861 } else {
8862 AvailableSpace::MinContent
8863 };
8864
8865 let mut element = (renderer.render)(&mut ChunkRendererContext {
8866 context: cx,
8867 window,
8868 max_width: text_width,
8869 });
8870 let line_height = text_style.line_height_in_pixels(window.rem_size());
8871 let size = element.layout_as_root(
8872 size(available_width, AvailableSpace::Definite(line_height)),
8873 window,
8874 cx,
8875 );
8876
8877 width += size.width;
8878 len += highlighted_chunk.text.len();
8879 fragments.push(LineFragment::Element {
8880 id: renderer.id,
8881 element: Some(element),
8882 size,
8883 len: highlighted_chunk.text.len(),
8884 });
8885 }
8886 ChunkReplacement::Str(x) => {
8887 let text_style = if let Some(style) = highlighted_chunk.style {
8888 Cow::Owned(text_style.clone().highlight(style))
8889 } else {
8890 Cow::Borrowed(text_style)
8891 };
8892
8893 let run = TextRun {
8894 len: x.len(),
8895 font: text_style.font(),
8896 color: text_style.color,
8897 background_color: text_style.background_color,
8898 underline: text_style.underline,
8899 strikethrough: text_style.strikethrough,
8900 };
8901 let line_layout = window
8902 .text_system()
8903 .shape_line(x, font_size, &[run], None)
8904 .with_len(highlighted_chunk.text.len());
8905
8906 width += line_layout.width;
8907 len += highlighted_chunk.text.len();
8908 fragments.push(LineFragment::Text(line_layout))
8909 }
8910 }
8911 } else {
8912 for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
8913 if ix > 0 {
8914 let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
8915 let text_runs = if segments.is_empty() {
8916 &styles
8917 } else {
8918 &Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
8919 };
8920 let shaped_line = window.text_system().shape_line(
8921 line.clone().into(),
8922 font_size,
8923 text_runs,
8924 None,
8925 );
8926 width += shaped_line.width;
8927 len += shaped_line.len;
8928 fragments.push(LineFragment::Text(shaped_line));
8929 layouts.push(Self {
8930 width: mem::take(&mut width),
8931 len: mem::take(&mut len),
8932 fragments: mem::take(&mut fragments),
8933 invisibles: std::mem::take(&mut invisibles),
8934 font_size,
8935 });
8936
8937 line.clear();
8938 styles.clear();
8939 row += 1;
8940 line_exceeded_max_len = false;
8941 non_whitespace_added = false;
8942 if row == max_line_count {
8943 return layouts;
8944 }
8945 }
8946
8947 if !line_chunk.is_empty() && !line_exceeded_max_len {
8948 let text_style = if let Some(style) = highlighted_chunk.style {
8949 Cow::Owned(text_style.clone().highlight(style))
8950 } else {
8951 Cow::Borrowed(text_style)
8952 };
8953
8954 if line.len() + line_chunk.len() > max_line_len {
8955 let mut chunk_len = max_line_len - line.len();
8956 while !line_chunk.is_char_boundary(chunk_len) {
8957 chunk_len -= 1;
8958 }
8959 line_chunk = &line_chunk[..chunk_len];
8960 line_exceeded_max_len = true;
8961 }
8962
8963 styles.push(TextRun {
8964 len: line_chunk.len(),
8965 font: text_style.font(),
8966 color: text_style.color,
8967 background_color: text_style.background_color,
8968 underline: text_style.underline,
8969 strikethrough: text_style.strikethrough,
8970 });
8971
8972 if editor_mode.is_full() && !highlighted_chunk.is_inlay {
8973 // Line wrap pads its contents with fake whitespaces,
8974 // avoid printing them
8975 let is_soft_wrapped = is_row_soft_wrapped(row);
8976 if highlighted_chunk.is_tab {
8977 if non_whitespace_added || !is_soft_wrapped {
8978 invisibles.push(Invisible::Tab {
8979 line_start_offset: line.len(),
8980 line_end_offset: line.len() + line_chunk.len(),
8981 });
8982 }
8983 } else {
8984 invisibles.extend(line_chunk.char_indices().filter_map(
8985 |(index, c)| {
8986 let is_whitespace = c.is_whitespace();
8987 non_whitespace_added |= !is_whitespace;
8988 if is_whitespace
8989 && (non_whitespace_added || !is_soft_wrapped)
8990 {
8991 Some(Invisible::Whitespace {
8992 line_offset: line.len() + index,
8993 })
8994 } else {
8995 None
8996 }
8997 },
8998 ))
8999 }
9000 }
9001
9002 line.push_str(line_chunk);
9003 }
9004 }
9005 }
9006 }
9007
9008 layouts
9009 }
9010
9011 /// Takes text runs and non-overlapping left-to-right background ranges with color.
9012 /// Returns new text runs with adjusted contrast as per background ranges.
9013 fn split_runs_by_bg_segments(
9014 text_runs: &[TextRun],
9015 bg_segments: &[(Range<DisplayPoint>, Hsla)],
9016 min_contrast: f32,
9017 start_col_offset: usize,
9018 ) -> Vec<TextRun> {
9019 let mut output_runs: Vec<TextRun> = Vec::with_capacity(text_runs.len());
9020 let mut line_col = start_col_offset;
9021 let mut segment_ix = 0usize;
9022
9023 for text_run in text_runs.iter() {
9024 let run_start_col = line_col;
9025 let run_end_col = run_start_col + text_run.len;
9026 while segment_ix < bg_segments.len()
9027 && (bg_segments[segment_ix].0.end.column() as usize) <= run_start_col
9028 {
9029 segment_ix += 1;
9030 }
9031 let mut cursor_col = run_start_col;
9032 let mut local_segment_ix = segment_ix;
9033 while local_segment_ix < bg_segments.len() {
9034 let (range, segment_color) = &bg_segments[local_segment_ix];
9035 let segment_start_col = range.start.column() as usize;
9036 let segment_end_col = range.end.column() as usize;
9037 if segment_start_col >= run_end_col {
9038 break;
9039 }
9040 if segment_start_col > cursor_col {
9041 let span_len = segment_start_col - cursor_col;
9042 output_runs.push(TextRun {
9043 len: span_len,
9044 font: text_run.font.clone(),
9045 color: text_run.color,
9046 background_color: text_run.background_color,
9047 underline: text_run.underline,
9048 strikethrough: text_run.strikethrough,
9049 });
9050 cursor_col = segment_start_col;
9051 }
9052 let segment_slice_end_col = segment_end_col.min(run_end_col);
9053 if segment_slice_end_col > cursor_col {
9054 let new_text_color =
9055 ensure_minimum_contrast(text_run.color, *segment_color, min_contrast);
9056 output_runs.push(TextRun {
9057 len: segment_slice_end_col - cursor_col,
9058 font: text_run.font.clone(),
9059 color: new_text_color,
9060 background_color: text_run.background_color,
9061 underline: text_run.underline,
9062 strikethrough: text_run.strikethrough,
9063 });
9064 cursor_col = segment_slice_end_col;
9065 }
9066 if segment_end_col >= run_end_col {
9067 break;
9068 }
9069 local_segment_ix += 1;
9070 }
9071 if cursor_col < run_end_col {
9072 output_runs.push(TextRun {
9073 len: run_end_col - cursor_col,
9074 font: text_run.font.clone(),
9075 color: text_run.color,
9076 background_color: text_run.background_color,
9077 underline: text_run.underline,
9078 strikethrough: text_run.strikethrough,
9079 });
9080 }
9081 line_col = run_end_col;
9082 segment_ix = local_segment_ix;
9083 }
9084 output_runs
9085 }
9086
9087 fn prepaint(
9088 &mut self,
9089 line_height: Pixels,
9090 scroll_position: gpui::Point<ScrollOffset>,
9091 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
9092 row: DisplayRow,
9093 content_origin: gpui::Point<Pixels>,
9094 line_elements: &mut SmallVec<[AnyElement; 1]>,
9095 window: &mut Window,
9096 cx: &mut App,
9097 ) {
9098 let line_y = f32::from(line_height) * Pixels::from(row.as_f64() - scroll_position.y);
9099 self.prepaint_with_custom_offset(
9100 line_height,
9101 scroll_pixel_position,
9102 content_origin,
9103 line_y,
9104 line_elements,
9105 window,
9106 cx,
9107 );
9108 }
9109
9110 fn prepaint_with_custom_offset(
9111 &mut self,
9112 line_height: Pixels,
9113 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
9114 content_origin: gpui::Point<Pixels>,
9115 line_y: Pixels,
9116 line_elements: &mut SmallVec<[AnyElement; 1]>,
9117 window: &mut Window,
9118 cx: &mut App,
9119 ) {
9120 let mut fragment_origin =
9121 content_origin + gpui::point(Pixels::from(-scroll_pixel_position.x), line_y);
9122 for fragment in &mut self.fragments {
9123 match fragment {
9124 LineFragment::Text(line) => {
9125 fragment_origin.x += line.width;
9126 }
9127 LineFragment::Element { element, size, .. } => {
9128 let mut element = element
9129 .take()
9130 .expect("you can't prepaint LineWithInvisibles twice");
9131
9132 // Center the element vertically within the line.
9133 let mut element_origin = fragment_origin;
9134 element_origin.y += (line_height - size.height) / 2.;
9135 element.prepaint_at(element_origin, window, cx);
9136 line_elements.push(element);
9137
9138 fragment_origin.x += size.width;
9139 }
9140 }
9141 }
9142 }
9143
9144 fn draw(
9145 &self,
9146 layout: &EditorLayout,
9147 row: DisplayRow,
9148 content_origin: gpui::Point<Pixels>,
9149 whitespace_setting: ShowWhitespaceSetting,
9150 selection_ranges: &[Range<DisplayPoint>],
9151 window: &mut Window,
9152 cx: &mut App,
9153 ) {
9154 self.draw_with_custom_offset(
9155 layout,
9156 row,
9157 content_origin,
9158 layout.position_map.line_height
9159 * (row.as_f64() - layout.position_map.scroll_position.y) as f32,
9160 whitespace_setting,
9161 selection_ranges,
9162 window,
9163 cx,
9164 );
9165 }
9166
9167 fn draw_with_custom_offset(
9168 &self,
9169 layout: &EditorLayout,
9170 row: DisplayRow,
9171 content_origin: gpui::Point<Pixels>,
9172 line_y: Pixels,
9173 whitespace_setting: ShowWhitespaceSetting,
9174 selection_ranges: &[Range<DisplayPoint>],
9175 window: &mut Window,
9176 cx: &mut App,
9177 ) {
9178 let line_height = layout.position_map.line_height;
9179 let mut fragment_origin = content_origin
9180 + gpui::point(
9181 Pixels::from(-layout.position_map.scroll_pixel_position.x),
9182 line_y,
9183 );
9184
9185 for fragment in &self.fragments {
9186 match fragment {
9187 LineFragment::Text(line) => {
9188 line.paint(
9189 fragment_origin,
9190 line_height,
9191 layout.text_align,
9192 Some(layout.content_width),
9193 window,
9194 cx,
9195 )
9196 .log_err();
9197 fragment_origin.x += line.width;
9198 }
9199 LineFragment::Element { size, .. } => {
9200 fragment_origin.x += size.width;
9201 }
9202 }
9203 }
9204
9205 self.draw_invisibles(
9206 selection_ranges,
9207 layout,
9208 content_origin,
9209 line_y,
9210 row,
9211 line_height,
9212 whitespace_setting,
9213 window,
9214 cx,
9215 );
9216 }
9217
9218 fn draw_background(
9219 &self,
9220 layout: &EditorLayout,
9221 row: DisplayRow,
9222 content_origin: gpui::Point<Pixels>,
9223 window: &mut Window,
9224 cx: &mut App,
9225 ) {
9226 let line_height = layout.position_map.line_height;
9227 let line_y = line_height * (row.as_f64() - layout.position_map.scroll_position.y) as f32;
9228
9229 let mut fragment_origin = content_origin
9230 + gpui::point(
9231 Pixels::from(-layout.position_map.scroll_pixel_position.x),
9232 line_y,
9233 );
9234
9235 for fragment in &self.fragments {
9236 match fragment {
9237 LineFragment::Text(line) => {
9238 line.paint_background(
9239 fragment_origin,
9240 line_height,
9241 layout.text_align,
9242 Some(layout.content_width),
9243 window,
9244 cx,
9245 )
9246 .log_err();
9247 fragment_origin.x += line.width;
9248 }
9249 LineFragment::Element { size, .. } => {
9250 fragment_origin.x += size.width;
9251 }
9252 }
9253 }
9254 }
9255
9256 fn draw_invisibles(
9257 &self,
9258 selection_ranges: &[Range<DisplayPoint>],
9259 layout: &EditorLayout,
9260 content_origin: gpui::Point<Pixels>,
9261 line_y: Pixels,
9262 row: DisplayRow,
9263 line_height: Pixels,
9264 whitespace_setting: ShowWhitespaceSetting,
9265 window: &mut Window,
9266 cx: &mut App,
9267 ) {
9268 let extract_whitespace_info = |invisible: &Invisible| {
9269 let (token_offset, token_end_offset, invisible_symbol) = match invisible {
9270 Invisible::Tab {
9271 line_start_offset,
9272 line_end_offset,
9273 } => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
9274 Invisible::Whitespace { line_offset } => {
9275 (*line_offset, line_offset + 1, &layout.space_invisible)
9276 }
9277 };
9278
9279 let x_offset: ScrollPixelOffset = self.x_for_index(token_offset).into();
9280 let invisible_offset: ScrollPixelOffset =
9281 ((layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0)
9282 .into();
9283 let origin = content_origin
9284 + gpui::point(
9285 Pixels::from(
9286 x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x,
9287 ),
9288 line_y,
9289 );
9290
9291 (
9292 [token_offset, token_end_offset],
9293 Box::new(move |window: &mut Window, cx: &mut App| {
9294 invisible_symbol
9295 .paint(origin, line_height, TextAlign::Left, None, window, cx)
9296 .log_err();
9297 }),
9298 )
9299 };
9300
9301 let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
9302 match whitespace_setting {
9303 ShowWhitespaceSetting::None => (),
9304 ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(window, cx)),
9305 ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
9306 let invisible_point = DisplayPoint::new(row, start as u32);
9307 if !selection_ranges
9308 .iter()
9309 .any(|region| region.start <= invisible_point && invisible_point < region.end)
9310 {
9311 return;
9312 }
9313
9314 paint(window, cx);
9315 }),
9316
9317 ShowWhitespaceSetting::Trailing => {
9318 let mut previous_start = self.len;
9319 for ([start, end], paint) in invisible_iter.rev() {
9320 if previous_start != end {
9321 break;
9322 }
9323 previous_start = start;
9324 paint(window, cx);
9325 }
9326 }
9327
9328 // For a whitespace to be on a boundary, any of the following conditions need to be met:
9329 // - It is a tab
9330 // - It is adjacent to an edge (start or end)
9331 // - It is adjacent to a whitespace (left or right)
9332 ShowWhitespaceSetting::Boundary => {
9333 // 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
9334 // the above cases.
9335 // Note: We zip in the original `invisibles` to check for tab equality
9336 let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut Window, &mut App)>)> = None;
9337 for (([start, end], paint), invisible) in
9338 invisible_iter.zip_eq(self.invisibles.iter())
9339 {
9340 let should_render = match (&last_seen, invisible) {
9341 (_, Invisible::Tab { .. }) => true,
9342 (Some((_, last_end, _)), _) => *last_end == start,
9343 _ => false,
9344 };
9345
9346 if should_render || start == 0 || end == self.len {
9347 paint(window, cx);
9348
9349 // Since we are scanning from the left, we will skip over the first available whitespace that is part
9350 // of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
9351 if let Some((should_render_last, last_end, paint_last)) = last_seen {
9352 // Note that we need to make sure that the last one is actually adjacent
9353 if !should_render_last && last_end == start {
9354 paint_last(window, cx);
9355 }
9356 }
9357 }
9358
9359 // Manually render anything within a selection
9360 let invisible_point = DisplayPoint::new(row, start as u32);
9361 if selection_ranges.iter().any(|region| {
9362 region.start <= invisible_point && invisible_point < region.end
9363 }) {
9364 paint(window, cx);
9365 }
9366
9367 last_seen = Some((should_render, end, paint));
9368 }
9369 }
9370 }
9371 }
9372
9373 pub fn x_for_index(&self, index: usize) -> Pixels {
9374 let mut fragment_start_x = Pixels::ZERO;
9375 let mut fragment_start_index = 0;
9376
9377 for fragment in &self.fragments {
9378 match fragment {
9379 LineFragment::Text(shaped_line) => {
9380 let fragment_end_index = fragment_start_index + shaped_line.len;
9381 if index < fragment_end_index {
9382 return fragment_start_x
9383 + shaped_line.x_for_index(index - fragment_start_index);
9384 }
9385 fragment_start_x += shaped_line.width;
9386 fragment_start_index = fragment_end_index;
9387 }
9388 LineFragment::Element { len, size, .. } => {
9389 let fragment_end_index = fragment_start_index + len;
9390 if index < fragment_end_index {
9391 return fragment_start_x;
9392 }
9393 fragment_start_x += size.width;
9394 fragment_start_index = fragment_end_index;
9395 }
9396 }
9397 }
9398
9399 fragment_start_x
9400 }
9401
9402 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
9403 let mut fragment_start_x = Pixels::ZERO;
9404 let mut fragment_start_index = 0;
9405
9406 for fragment in &self.fragments {
9407 match fragment {
9408 LineFragment::Text(shaped_line) => {
9409 let fragment_end_x = fragment_start_x + shaped_line.width;
9410 if x < fragment_end_x {
9411 return Some(
9412 fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
9413 );
9414 }
9415 fragment_start_x = fragment_end_x;
9416 fragment_start_index += shaped_line.len;
9417 }
9418 LineFragment::Element { len, size, .. } => {
9419 let fragment_end_x = fragment_start_x + size.width;
9420 if x < fragment_end_x {
9421 return Some(fragment_start_index);
9422 }
9423 fragment_start_index += len;
9424 fragment_start_x = fragment_end_x;
9425 }
9426 }
9427 }
9428
9429 None
9430 }
9431
9432 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
9433 let mut fragment_start_index = 0;
9434
9435 for fragment in &self.fragments {
9436 match fragment {
9437 LineFragment::Text(shaped_line) => {
9438 let fragment_end_index = fragment_start_index + shaped_line.len;
9439 if index < fragment_end_index {
9440 return shaped_line.font_id_for_index(index - fragment_start_index);
9441 }
9442 fragment_start_index = fragment_end_index;
9443 }
9444 LineFragment::Element { len, .. } => {
9445 let fragment_end_index = fragment_start_index + len;
9446 if index < fragment_end_index {
9447 return None;
9448 }
9449 fragment_start_index = fragment_end_index;
9450 }
9451 }
9452 }
9453
9454 None
9455 }
9456
9457 pub fn alignment_offset(&self, text_align: TextAlign, content_width: Pixels) -> Pixels {
9458 let line_width = self.width;
9459 match text_align {
9460 TextAlign::Left => px(0.0),
9461 TextAlign::Center => (content_width - line_width) / 2.0,
9462 TextAlign::Right => content_width - line_width,
9463 }
9464 }
9465}
9466
9467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9468enum Invisible {
9469 /// A tab character
9470 ///
9471 /// A tab character is internally represented by spaces (configured by the user's tab width)
9472 /// aligned to the nearest column, so it's necessary to store the start and end offset for
9473 /// adjacency checks.
9474 Tab {
9475 line_start_offset: usize,
9476 line_end_offset: usize,
9477 },
9478 Whitespace {
9479 line_offset: usize,
9480 },
9481}
9482
9483impl EditorElement {
9484 /// Returns the rem size to use when rendering the [`EditorElement`].
9485 ///
9486 /// This allows UI elements to scale based on the `buffer_font_size`.
9487 fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
9488 match self.editor.read(cx).mode {
9489 EditorMode::Full {
9490 scale_ui_elements_with_buffer_font_size: true,
9491 ..
9492 }
9493 | EditorMode::Minimap { .. } => {
9494 let buffer_font_size = self.style.text.font_size;
9495 match buffer_font_size {
9496 AbsoluteLength::Pixels(pixels) => {
9497 let rem_size_scale = {
9498 // Our default UI font size is 14px on a 16px base scale.
9499 // This means the default UI font size is 0.875rems.
9500 let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
9501
9502 // We then determine the delta between a single rem and the default font
9503 // size scale.
9504 let default_font_size_delta = 1. - default_font_size_scale;
9505
9506 // Finally, we add this delta to 1rem to get the scale factor that
9507 // should be used to scale up the UI.
9508 1. + default_font_size_delta
9509 };
9510
9511 Some(pixels * rem_size_scale)
9512 }
9513 AbsoluteLength::Rems(rems) => {
9514 Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into()))
9515 }
9516 }
9517 }
9518 // We currently use single-line and auto-height editors in UI contexts,
9519 // so we don't want to scale everything with the buffer font size, as it
9520 // ends up looking off.
9521 _ => None,
9522 }
9523 }
9524
9525 fn editor_with_selections(&self, cx: &App) -> Option<Entity<Editor>> {
9526 if let EditorMode::Minimap { parent } = self.editor.read(cx).mode() {
9527 parent.upgrade()
9528 } else {
9529 Some(self.editor.clone())
9530 }
9531 }
9532}
9533
9534#[derive(Default)]
9535pub struct EditorRequestLayoutState {
9536 // We use prepaint depth to limit the number of times prepaint is
9537 // called recursively. We need this so that we can update stale
9538 // data for e.g. block heights in block map.
9539 prepaint_depth: Rc<Cell<usize>>,
9540}
9541
9542impl EditorRequestLayoutState {
9543 // In ideal conditions we only need one more subsequent prepaint call for resize to take effect.
9544 // i.e. MAX_PREPAINT_DEPTH = 2, but since moving blocks inline (place_near), more lines from
9545 // below get exposed, and we end up querying blocks for those lines too in subsequent renders.
9546 // Setting MAX_PREPAINT_DEPTH = 3, passes all tests. Just to be on the safe side we set it to 5, so
9547 // that subsequent shrinking does not lead to incorrect block placing.
9548 const MAX_PREPAINT_DEPTH: usize = 5;
9549
9550 fn increment_prepaint_depth(&self) -> EditorPrepaintGuard {
9551 let depth = self.prepaint_depth.get();
9552 self.prepaint_depth.set(depth + 1);
9553 EditorPrepaintGuard {
9554 prepaint_depth: self.prepaint_depth.clone(),
9555 }
9556 }
9557
9558 fn has_remaining_prepaint_depth(&self) -> bool {
9559 self.prepaint_depth.get() < Self::MAX_PREPAINT_DEPTH
9560 }
9561}
9562
9563struct EditorPrepaintGuard {
9564 prepaint_depth: Rc<Cell<usize>>,
9565}
9566
9567impl Drop for EditorPrepaintGuard {
9568 fn drop(&mut self) {
9569 let depth = self.prepaint_depth.get();
9570 self.prepaint_depth.set(depth.saturating_sub(1));
9571 }
9572}
9573
9574impl Element for EditorElement {
9575 type RequestLayoutState = EditorRequestLayoutState;
9576 type PrepaintState = EditorLayout;
9577
9578 fn id(&self) -> Option<ElementId> {
9579 None
9580 }
9581
9582 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
9583 None
9584 }
9585
9586 fn request_layout(
9587 &mut self,
9588 _: Option<&GlobalElementId>,
9589 _inspector_id: Option<&gpui::InspectorElementId>,
9590 window: &mut Window,
9591 cx: &mut App,
9592 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
9593 let rem_size = self.rem_size(cx);
9594 window.with_rem_size(rem_size, |window| {
9595 self.editor.update(cx, |editor, cx| {
9596 editor.set_style(self.style.clone(), window, cx);
9597
9598 let layout_id = match editor.mode {
9599 EditorMode::SingleLine => {
9600 let rem_size = window.rem_size();
9601 let height = self.style.text.line_height_in_pixels(rem_size);
9602 let mut style = Style::default();
9603 style.size.height = height.into();
9604 style.size.width = relative(1.).into();
9605 window.request_layout(style, None, cx)
9606 }
9607 EditorMode::AutoHeight {
9608 min_lines,
9609 max_lines,
9610 } => {
9611 let editor_handle = cx.entity();
9612 window.request_measured_layout(
9613 Style::default(),
9614 move |known_dimensions, available_space, window, cx| {
9615 editor_handle
9616 .update(cx, |editor, cx| {
9617 compute_auto_height_layout(
9618 editor,
9619 min_lines,
9620 max_lines,
9621 known_dimensions,
9622 available_space.width,
9623 window,
9624 cx,
9625 )
9626 })
9627 .unwrap_or_default()
9628 },
9629 )
9630 }
9631 EditorMode::Minimap { .. } => {
9632 let mut style = Style::default();
9633 style.size.width = relative(1.).into();
9634 style.size.height = relative(1.).into();
9635 window.request_layout(style, None, cx)
9636 }
9637 EditorMode::Full {
9638 sizing_behavior, ..
9639 } => {
9640 let mut style = Style::default();
9641 style.size.width = relative(1.).into();
9642 if sizing_behavior == SizingBehavior::SizeByContent {
9643 let snapshot = editor.snapshot(window, cx);
9644 let line_height =
9645 self.style.text.line_height_in_pixels(window.rem_size());
9646 let scroll_height =
9647 (snapshot.max_point().row().next_row().0 as f32) * line_height;
9648 style.size.height = scroll_height.into();
9649 } else {
9650 style.size.height = relative(1.).into();
9651 }
9652 window.request_layout(style, None, cx)
9653 }
9654 };
9655
9656 (layout_id, EditorRequestLayoutState::default())
9657 })
9658 })
9659 }
9660
9661 fn prepaint(
9662 &mut self,
9663 _: Option<&GlobalElementId>,
9664 _inspector_id: Option<&gpui::InspectorElementId>,
9665 bounds: Bounds<Pixels>,
9666 request_layout: &mut Self::RequestLayoutState,
9667 window: &mut Window,
9668 cx: &mut App,
9669 ) -> Self::PrepaintState {
9670 let _prepaint_depth_guard = request_layout.increment_prepaint_depth();
9671 let text_style = TextStyleRefinement {
9672 font_size: Some(self.style.text.font_size),
9673 line_height: Some(self.style.text.line_height),
9674 ..Default::default()
9675 };
9676
9677 let is_minimap = self.editor.read(cx).mode.is_minimap();
9678 let is_singleton = self.editor.read(cx).buffer_kind(cx) == ItemBufferKind::Singleton;
9679
9680 if !is_minimap {
9681 let focus_handle = self.editor.focus_handle(cx);
9682 window.set_view_id(self.editor.entity_id());
9683 window.set_focus_handle(&focus_handle, cx);
9684 }
9685
9686 let rem_size = self.rem_size(cx);
9687 window.with_rem_size(rem_size, |window| {
9688 window.with_text_style(Some(text_style), |window| {
9689 window.with_content_mask(Some(ContentMask { bounds }), |window| {
9690 let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
9691 (editor.snapshot(window, cx), editor.read_only(cx))
9692 });
9693 let style = &self.style;
9694
9695 let rem_size = window.rem_size();
9696 let font_id = window.text_system().resolve_font(&style.text.font());
9697 let font_size = style.text.font_size.to_pixels(rem_size);
9698 let line_height = style.text.line_height_in_pixels(rem_size);
9699 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
9700 let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
9701 let em_layout_width = window.text_system().em_layout_width(font_id, font_size);
9702 let glyph_grid_cell = size(em_advance, line_height);
9703
9704 let gutter_dimensions =
9705 snapshot.gutter_dimensions(font_id, font_size, style, window, cx);
9706 let text_width = bounds.size.width - gutter_dimensions.width;
9707
9708 let settings = EditorSettings::get_global(cx);
9709 let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never;
9710 let vertical_scrollbar_width = (scrollbars_shown
9711 && settings.scrollbar.axes.vertical
9712 && self.editor.read(cx).show_scrollbars.vertical)
9713 .then_some(style.scrollbar_width)
9714 .unwrap_or_default();
9715 let minimap_width = self
9716 .get_minimap_width(
9717 &settings.minimap,
9718 scrollbars_shown,
9719 text_width,
9720 em_width,
9721 font_size,
9722 rem_size,
9723 cx,
9724 )
9725 .unwrap_or_default();
9726
9727 let right_margin = minimap_width + vertical_scrollbar_width;
9728
9729 let extended_right = 2 * em_width + right_margin;
9730 let editor_width = text_width - gutter_dimensions.margin - extended_right;
9731 let editor_margins = EditorMargins {
9732 gutter: gutter_dimensions,
9733 right: right_margin,
9734 extended_right,
9735 };
9736
9737 snapshot = self.editor.update(cx, |editor, cx| {
9738 editor.last_bounds = Some(bounds);
9739 editor.gutter_dimensions = gutter_dimensions;
9740 editor.set_visible_line_count(
9741 (bounds.size.height / line_height) as f64,
9742 window,
9743 cx,
9744 );
9745 editor.set_visible_column_count(f64::from(editor_width / em_advance));
9746
9747 if matches!(
9748 editor.mode,
9749 EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
9750 ) {
9751 snapshot
9752 } else {
9753 let wrap_width = calculate_wrap_width(
9754 editor.soft_wrap_mode(cx),
9755 editor_width,
9756 em_layout_width,
9757 );
9758
9759 if editor.set_wrap_width(wrap_width, cx) {
9760 editor.snapshot(window, cx)
9761 } else {
9762 snapshot
9763 }
9764 }
9765 });
9766
9767 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
9768 let gutter_hitbox = window.insert_hitbox(
9769 gutter_bounds(bounds, gutter_dimensions),
9770 HitboxBehavior::Normal,
9771 );
9772 let text_hitbox = window.insert_hitbox(
9773 Bounds {
9774 origin: gutter_hitbox.top_right(),
9775 size: size(text_width, bounds.size.height),
9776 },
9777 HitboxBehavior::Normal,
9778 );
9779
9780 // Offset the content_bounds from the text_bounds by the gutter margin (which
9781 // is roughly half a character wide) to make hit testing work more like how we want.
9782 let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO);
9783 let content_origin = text_hitbox.origin + content_offset;
9784
9785 let height_in_lines = f64::from(bounds.size.height / line_height);
9786 let max_row = snapshot.max_point().row().as_f64();
9787
9788 // Calculate how much of the editor is clipped by parent containers (e.g., List).
9789 // This allows us to only render lines that are actually visible, which is
9790 // critical for performance when large AutoHeight editors are inside Lists.
9791 let visible_bounds = window.content_mask().bounds;
9792 let clipped_top = (visible_bounds.origin.y - bounds.origin.y).max(px(0.));
9793 let clipped_top_in_lines = f64::from(clipped_top / line_height);
9794 let visible_height_in_lines =
9795 f64::from(visible_bounds.size.height / line_height);
9796
9797 // The max scroll position for the top of the window
9798 let scroll_beyond_last_line = self.editor.read(cx).scroll_beyond_last_line(cx);
9799 let max_scroll_top = match scroll_beyond_last_line {
9800 ScrollBeyondLastLine::OnePage => max_row,
9801 ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.).max(0.),
9802 ScrollBeyondLastLine::VerticalScrollMargin => {
9803 let settings = EditorSettings::get_global(cx);
9804 (max_row - height_in_lines + 1. + settings.vertical_scroll_margin)
9805 .max(0.)
9806 }
9807 };
9808
9809 let (
9810 autoscroll_request,
9811 autoscroll_containing_element,
9812 needs_horizontal_autoscroll,
9813 ) = self.editor.update(cx, |editor, cx| {
9814 let autoscroll_request = editor.scroll_manager.take_autoscroll_request();
9815
9816 let autoscroll_containing_element =
9817 autoscroll_request.is_some() || editor.has_pending_selection();
9818
9819 let (needs_horizontal_autoscroll, was_scrolled) = editor
9820 .autoscroll_vertically(
9821 bounds,
9822 line_height,
9823 max_scroll_top,
9824 autoscroll_request,
9825 window,
9826 cx,
9827 );
9828 if was_scrolled.0 {
9829 snapshot = editor.snapshot(window, cx);
9830 }
9831 (
9832 autoscroll_request,
9833 autoscroll_containing_element,
9834 needs_horizontal_autoscroll,
9835 )
9836 });
9837
9838 let mut scroll_position = snapshot.scroll_position();
9839 // The scroll position is a fractional point, the whole number of which represents
9840 // the top of the window in terms of display rows.
9841 // We add clipped_top_in_lines to skip rows that are clipped by parent containers,
9842 // but we don't modify scroll_position itself since the parent handles positioning.
9843 let max_row = snapshot.max_point().row();
9844 let start_row = cmp::min(
9845 DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32),
9846 max_row,
9847 );
9848 let end_row = cmp::min(
9849 (scroll_position.y + clipped_top_in_lines + visible_height_in_lines).ceil()
9850 as u32,
9851 max_row.next_row().0,
9852 );
9853 let end_row = DisplayRow(end_row);
9854
9855 let row_infos = snapshot // note we only get the visual range
9856 .row_infos(start_row)
9857 .take((start_row..end_row).len())
9858 .collect::<Vec<RowInfo>>();
9859 let is_row_soft_wrapped = |row: usize| {
9860 row_infos
9861 .get(row)
9862 .is_none_or(|info| info.buffer_row.is_none())
9863 };
9864
9865 let start_anchor = if start_row == Default::default() {
9866 Anchor::Min
9867 } else {
9868 snapshot.buffer_snapshot().anchor_before(
9869 DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
9870 )
9871 };
9872 let end_anchor = if end_row > max_row {
9873 Anchor::Max
9874 } else {
9875 snapshot.buffer_snapshot().anchor_before(
9876 DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
9877 )
9878 };
9879
9880 let mut highlighted_rows = self
9881 .editor
9882 .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
9883
9884 let is_light = cx.theme().appearance().is_light();
9885
9886 let mut highlighted_ranges = self
9887 .editor_with_selections(cx)
9888 .map(|editor| {
9889 if editor == self.editor {
9890 editor.read(cx).background_highlights_in_range(
9891 start_anchor..end_anchor,
9892 &snapshot.display_snapshot,
9893 cx.theme(),
9894 )
9895 } else {
9896 editor.update(cx, |editor, cx| {
9897 let snapshot = editor.snapshot(window, cx);
9898 let start_anchor = if start_row == Default::default() {
9899 Anchor::Min
9900 } else {
9901 snapshot.buffer_snapshot().anchor_before(
9902 DisplayPoint::new(start_row, 0)
9903 .to_offset(&snapshot, Bias::Left),
9904 )
9905 };
9906 let end_anchor = if end_row > max_row {
9907 Anchor::Max
9908 } else {
9909 snapshot.buffer_snapshot().anchor_before(
9910 DisplayPoint::new(end_row, 0)
9911 .to_offset(&snapshot, Bias::Right),
9912 )
9913 };
9914
9915 editor.background_highlights_in_range(
9916 start_anchor..end_anchor,
9917 &snapshot.display_snapshot,
9918 cx.theme(),
9919 )
9920 })
9921 }
9922 })
9923 .unwrap_or_default();
9924
9925 for (ix, row_info) in row_infos.iter().enumerate() {
9926 let Some(diff_status) = row_info.diff_status else {
9927 continue;
9928 };
9929
9930 let background_color = match diff_status.kind {
9931 DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
9932 DiffHunkStatusKind::Deleted => {
9933 cx.theme().colors().version_control_deleted
9934 }
9935 DiffHunkStatusKind::Modified => {
9936 debug_panic!("modified diff status for row info");
9937 continue;
9938 }
9939 };
9940
9941 let hunk_opacity = if is_light { 0.16 } else { 0.12 };
9942
9943 let hollow_highlight = LineHighlight {
9944 background: (background_color.opacity(if is_light {
9945 0.08
9946 } else {
9947 0.06
9948 }))
9949 .into(),
9950 border: Some(if is_light {
9951 background_color.opacity(0.48)
9952 } else {
9953 background_color.opacity(0.36)
9954 }),
9955 include_gutter: true,
9956 type_id: None,
9957 };
9958
9959 let filled_highlight = LineHighlight {
9960 background: solid_background(background_color.opacity(hunk_opacity)),
9961 border: None,
9962 include_gutter: true,
9963 type_id: None,
9964 };
9965
9966 let background = if Self::diff_hunk_hollow(diff_status, cx) {
9967 hollow_highlight
9968 } else {
9969 filled_highlight
9970 };
9971
9972 let base_display_point =
9973 DisplayPoint::new(start_row + DisplayRow(ix as u32), 0);
9974
9975 highlighted_rows
9976 .entry(base_display_point.row())
9977 .or_insert(background);
9978 }
9979
9980 // Add diff review drag selection highlight to text area
9981 if let Some(drag_state) = &self.editor.read(cx).diff_review_drag_state {
9982 let range = drag_state.row_range(&snapshot.display_snapshot);
9983 let start_row = range.start().0;
9984 let end_row = range.end().0;
9985 let drag_highlight_color =
9986 cx.theme().colors().editor_active_line_background;
9987 let drag_highlight = LineHighlight {
9988 background: solid_background(drag_highlight_color),
9989 border: Some(cx.theme().colors().border_focused),
9990 include_gutter: true,
9991 type_id: None,
9992 };
9993 for row_num in start_row..=end_row {
9994 highlighted_rows
9995 .entry(DisplayRow(row_num))
9996 .or_insert(drag_highlight);
9997 }
9998 }
9999
10000 let highlighted_gutter_ranges =
10001 self.editor.read(cx).gutter_highlights_in_range(
10002 start_anchor..end_anchor,
10003 &snapshot.display_snapshot,
10004 cx,
10005 );
10006
10007 let document_colors = self
10008 .editor
10009 .read(cx)
10010 .colors
10011 .as_ref()
10012 .map(|colors| colors.editor_display_highlights(&snapshot));
10013 let redacted_ranges = self.editor.read(cx).redacted_ranges(
10014 start_anchor..end_anchor,
10015 &snapshot.display_snapshot,
10016 cx,
10017 );
10018
10019 let (local_selections, selected_buffer_ids, latest_selection_anchors): (
10020 Vec<Selection<Point>>,
10021 Vec<BufferId>,
10022 HashMap<BufferId, Anchor>,
10023 ) = self
10024 .editor_with_selections(cx)
10025 .map(|editor| {
10026 editor.update(cx, |editor, cx| {
10027 let all_selections =
10028 editor.selections.all::<Point>(&snapshot.display_snapshot);
10029 let all_anchor_selections =
10030 editor.selections.all_anchors(&snapshot.display_snapshot);
10031 let selected_buffer_ids =
10032 if editor.buffer_kind(cx) == ItemBufferKind::Singleton {
10033 Vec::new()
10034 } else {
10035 let mut selected_buffer_ids =
10036 Vec::with_capacity(all_selections.len());
10037
10038 for selection in all_selections {
10039 for buffer_id in snapshot
10040 .buffer_snapshot()
10041 .buffer_ids_for_range(selection.range())
10042 {
10043 if selected_buffer_ids.last() != Some(&buffer_id) {
10044 selected_buffer_ids.push(buffer_id);
10045 }
10046 }
10047 }
10048
10049 selected_buffer_ids
10050 };
10051
10052 let mut selections = editor.selections.disjoint_in_range(
10053 start_anchor..end_anchor,
10054 &snapshot.display_snapshot,
10055 );
10056 selections
10057 .extend(editor.selections.pending(&snapshot.display_snapshot));
10058
10059 let mut anchors_by_buffer: HashMap<BufferId, (usize, Anchor)> =
10060 HashMap::default();
10061 for selection in all_anchor_selections.iter() {
10062 let head = selection.head();
10063 if let Some((text_anchor, _)) =
10064 snapshot.buffer_snapshot().anchor_to_buffer_anchor(head)
10065 {
10066 anchors_by_buffer
10067 .entry(text_anchor.buffer_id)
10068 .and_modify(|(latest_id, latest_anchor)| {
10069 if selection.id > *latest_id {
10070 *latest_id = selection.id;
10071 *latest_anchor = head;
10072 }
10073 })
10074 .or_insert((selection.id, head));
10075 }
10076 }
10077 let latest_selection_anchors = anchors_by_buffer
10078 .into_iter()
10079 .map(|(buffer_id, (_, anchor))| (buffer_id, anchor))
10080 .collect();
10081
10082 (selections, selected_buffer_ids, latest_selection_anchors)
10083 })
10084 })
10085 .unwrap_or_else(|| (Vec::new(), Vec::new(), HashMap::default()));
10086
10087 let (selections, mut active_rows, newest_selection_head) = self
10088 .layout_selections(
10089 start_anchor,
10090 end_anchor,
10091 &local_selections,
10092 &snapshot,
10093 start_row,
10094 end_row,
10095 window,
10096 cx,
10097 );
10098
10099 // relative rows are based on newest selection, even outside the visible area
10100 let current_selection_head = self.editor.update(cx, |editor, cx| {
10101 (editor.selections.count() != 0).then(|| {
10102 let newest = editor
10103 .selections
10104 .newest::<Point>(&editor.display_snapshot(cx));
10105
10106 SelectionLayout::new(
10107 newest,
10108 editor.selections.line_mode(),
10109 editor.cursor_offset_on_selection,
10110 editor.cursor_shape,
10111 &snapshot,
10112 true,
10113 true,
10114 None,
10115 )
10116 .head
10117 .row()
10118 })
10119 });
10120
10121 let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
10122 editor.active_breakpoints(start_row..end_row, window, cx)
10123 });
10124 for (display_row, (_, bp, state)) in &breakpoint_rows {
10125 if bp.is_enabled() && state.is_none_or(|s| s.verified) {
10126 active_rows.entry(*display_row).or_default().breakpoint = true;
10127 }
10128 }
10129
10130 let line_numbers = self.layout_line_numbers(
10131 Some(&gutter_hitbox),
10132 gutter_dimensions,
10133 line_height,
10134 scroll_position,
10135 start_row..end_row,
10136 &row_infos,
10137 &active_rows,
10138 current_selection_head,
10139 &snapshot,
10140 window,
10141 cx,
10142 );
10143
10144 // We add the gutter breakpoint indicator to breakpoint_rows after painting
10145 // line numbers so we don't paint a line number debug accent color if a user
10146 // has their mouse over that line when a breakpoint isn't there
10147 self.editor.update(cx, |editor, _| {
10148 if let Some(phantom_breakpoint) = &mut editor
10149 .gutter_breakpoint_indicator
10150 .0
10151 .filter(|phantom_breakpoint| phantom_breakpoint.is_active)
10152 {
10153 // Is there a non-phantom breakpoint on this line?
10154 phantom_breakpoint.collides_with_existing_breakpoint = true;
10155 breakpoint_rows
10156 .entry(phantom_breakpoint.display_row)
10157 .or_insert_with(|| {
10158 let position = snapshot.display_point_to_anchor(
10159 DisplayPoint::new(phantom_breakpoint.display_row, 0),
10160 Bias::Right,
10161 );
10162 let breakpoint = Breakpoint::new_standard();
10163 phantom_breakpoint.collides_with_existing_breakpoint = false;
10164 (position, breakpoint, None)
10165 });
10166 }
10167 });
10168
10169 let mut expand_toggles =
10170 window.with_element_namespace("expand_toggles", |window| {
10171 self.layout_expand_toggles(
10172 &gutter_hitbox,
10173 gutter_dimensions,
10174 em_width,
10175 line_height,
10176 scroll_position,
10177 &row_infos,
10178 window,
10179 cx,
10180 )
10181 });
10182
10183 let mut crease_toggles =
10184 window.with_element_namespace("crease_toggles", |window| {
10185 self.layout_crease_toggles(
10186 start_row..end_row,
10187 &row_infos,
10188 &active_rows,
10189 &snapshot,
10190 window,
10191 cx,
10192 )
10193 });
10194 let crease_trailers =
10195 window.with_element_namespace("crease_trailers", |window| {
10196 self.layout_crease_trailers(
10197 row_infos.iter().cloned(),
10198 &snapshot,
10199 window,
10200 cx,
10201 )
10202 });
10203
10204 let display_hunks = self.layout_gutter_diff_hunks(
10205 line_height,
10206 &gutter_hitbox,
10207 start_row..end_row,
10208 &snapshot,
10209 window,
10210 cx,
10211 );
10212
10213 Self::layout_word_diff_highlights(
10214 &display_hunks,
10215 &row_infos,
10216 start_row,
10217 &snapshot,
10218 &mut highlighted_ranges,
10219 cx,
10220 );
10221
10222 let merged_highlighted_ranges =
10223 if let Some((_, colors)) = document_colors.as_ref() {
10224 &highlighted_ranges
10225 .clone()
10226 .into_iter()
10227 .chain(colors.clone())
10228 .collect()
10229 } else {
10230 &highlighted_ranges
10231 };
10232 let bg_segments_per_row = Self::bg_segments_per_row(
10233 start_row..end_row,
10234 &selections,
10235 &merged_highlighted_ranges,
10236 self.style.background,
10237 );
10238
10239 let mut line_layouts = Self::layout_lines(
10240 start_row..end_row,
10241 &snapshot,
10242 &self.style,
10243 editor_width,
10244 is_row_soft_wrapped,
10245 &bg_segments_per_row,
10246 window,
10247 cx,
10248 );
10249 let new_renderer_widths = (!is_minimap).then(|| {
10250 line_layouts
10251 .iter()
10252 .flat_map(|layout| &layout.fragments)
10253 .filter_map(|fragment| {
10254 if let LineFragment::Element { id, size, .. } = fragment {
10255 Some((*id, size.width))
10256 } else {
10257 None
10258 }
10259 })
10260 });
10261 let renderer_widths_changed = request_layout.has_remaining_prepaint_depth()
10262 && new_renderer_widths.is_some_and(|new_renderer_widths| {
10263 self.editor.update(cx, |editor, cx| {
10264 editor.update_renderer_widths(new_renderer_widths, cx)
10265 })
10266 });
10267 if renderer_widths_changed {
10268 return self.prepaint(
10269 None,
10270 _inspector_id,
10271 bounds,
10272 request_layout,
10273 window,
10274 cx,
10275 );
10276 }
10277
10278 let longest_line_blame_width = self
10279 .editor
10280 .update(cx, |editor, cx| {
10281 if !editor.show_git_blame_inline {
10282 return None;
10283 }
10284 let blame = editor.blame.as_ref()?;
10285 let (_, blame_entry) = blame
10286 .update(cx, |blame, cx| {
10287 let row_infos =
10288 snapshot.row_infos(snapshot.longest_row()).next()?;
10289 blame.blame_for_rows(&[row_infos], cx).next()
10290 })
10291 .flatten()?;
10292 let mut element = render_inline_blame_entry(blame_entry, style, cx)?;
10293 let inline_blame_padding =
10294 ProjectSettings::get_global(cx).git.inline_blame.padding as f32
10295 * em_advance;
10296 Some(
10297 element
10298 .layout_as_root(AvailableSpace::min_size(), window, cx)
10299 .width
10300 + inline_blame_padding,
10301 )
10302 })
10303 .unwrap_or(Pixels::ZERO);
10304
10305 let longest_line_width = layout_line(
10306 snapshot.longest_row(),
10307 &snapshot,
10308 style,
10309 editor_width,
10310 is_row_soft_wrapped,
10311 window,
10312 cx,
10313 )
10314 .width;
10315
10316 let scrollbar_layout_information = ScrollbarLayoutInformation::new(
10317 text_hitbox.bounds,
10318 glyph_grid_cell,
10319 size(
10320 longest_line_width,
10321 Pixels::from(max_row.as_f64() * f64::from(line_height)),
10322 ),
10323 longest_line_blame_width,
10324 EditorSettings::get_global(cx),
10325 scroll_beyond_last_line,
10326 );
10327
10328 let mut scroll_width = scrollbar_layout_information.scroll_range.width;
10329
10330 let sticky_header_excerpt = if snapshot.buffer_snapshot().show_headers() {
10331 snapshot.sticky_header_excerpt(scroll_position.y)
10332 } else {
10333 None
10334 };
10335 let sticky_header_excerpt_id = sticky_header_excerpt
10336 .as_ref()
10337 .map(|top| top.excerpt.buffer_id());
10338
10339 let buffer = snapshot.buffer_snapshot();
10340 let start_buffer_row = MultiBufferRow(start_anchor.to_point(&buffer).row);
10341 let end_buffer_row = MultiBufferRow(end_anchor.to_point(&buffer).row);
10342
10343 let preliminary_scroll_pixel_position = point(
10344 scroll_position.x * f64::from(em_layout_width),
10345 scroll_position.y * f64::from(line_height),
10346 );
10347 let indent_guides = self.layout_indent_guides(
10348 content_origin,
10349 text_hitbox.origin,
10350 start_buffer_row..end_buffer_row,
10351 preliminary_scroll_pixel_position,
10352 line_height,
10353 &snapshot,
10354 window,
10355 cx,
10356 );
10357 let indent_guides_for_spacers = indent_guides.clone();
10358
10359 let blocks = (!is_minimap)
10360 .then(|| {
10361 window.with_element_namespace("blocks", |window| {
10362 self.render_blocks(
10363 start_row..end_row,
10364 &snapshot,
10365 &hitbox,
10366 &text_hitbox,
10367 editor_width,
10368 &mut scroll_width,
10369 &editor_margins,
10370 em_width,
10371 gutter_dimensions.full_width(),
10372 line_height,
10373 &mut line_layouts,
10374 &local_selections,
10375 &selected_buffer_ids,
10376 &latest_selection_anchors,
10377 is_row_soft_wrapped,
10378 sticky_header_excerpt_id,
10379 &indent_guides_for_spacers,
10380 window,
10381 cx,
10382 )
10383 })
10384 })
10385 .unwrap_or_default();
10386 let RenderBlocksOutput {
10387 non_spacer_blocks: mut blocks,
10388 mut spacer_blocks,
10389 row_block_types,
10390 resized_blocks,
10391 } = blocks;
10392 if let Some(resized_blocks) = resized_blocks {
10393 if request_layout.has_remaining_prepaint_depth() {
10394 self.editor.update(cx, |editor, cx| {
10395 editor.resize_blocks(
10396 resized_blocks,
10397 autoscroll_request.map(|(autoscroll, _)| autoscroll),
10398 cx,
10399 )
10400 });
10401 return self.prepaint(
10402 None,
10403 _inspector_id,
10404 bounds,
10405 request_layout,
10406 window,
10407 cx,
10408 );
10409 } else {
10410 debug_panic!(
10411 "dropping block resize because prepaint depth \
10412 limit was reached"
10413 );
10414 }
10415 }
10416
10417 let sticky_buffer_header = if self.should_show_buffer_headers() {
10418 sticky_header_excerpt.map(|sticky_header_excerpt| {
10419 window.with_element_namespace("blocks", |window| {
10420 self.layout_sticky_buffer_header(
10421 sticky_header_excerpt,
10422 scroll_position,
10423 line_height,
10424 right_margin,
10425 &snapshot,
10426 &hitbox,
10427 &selected_buffer_ids,
10428 &blocks,
10429 &latest_selection_anchors,
10430 window,
10431 cx,
10432 )
10433 })
10434 })
10435 } else {
10436 None
10437 };
10438
10439 let scroll_max: gpui::Point<ScrollPixelOffset> = point(
10440 ScrollPixelOffset::from(
10441 ((scroll_width - editor_width) / em_layout_width).max(0.0),
10442 ),
10443 max_scroll_top,
10444 );
10445
10446 self.editor.update(cx, |editor, cx| {
10447 if editor.scroll_manager.clamp_scroll_left(scroll_max.x, cx) {
10448 scroll_position.x = scroll_max.x.min(scroll_position.x);
10449 }
10450
10451 if needs_horizontal_autoscroll.0
10452 && let Some(new_scroll_position) = editor.autoscroll_horizontally(
10453 start_row,
10454 editor_width,
10455 scroll_width,
10456 em_advance,
10457 &line_layouts,
10458 autoscroll_request,
10459 window,
10460 cx,
10461 )
10462 {
10463 scroll_position = new_scroll_position;
10464 }
10465 });
10466
10467 let scroll_pixel_position = point(
10468 scroll_position.x * f64::from(em_layout_width),
10469 scroll_position.y * f64::from(line_height),
10470 );
10471 let sticky_headers = if !is_minimap
10472 && is_singleton
10473 && EditorSettings::get_global(cx).sticky_scroll.enabled
10474 {
10475 let relative = self.editor.read(cx).relative_line_numbers(cx);
10476 self.layout_sticky_headers(
10477 &snapshot,
10478 editor_width,
10479 is_row_soft_wrapped,
10480 line_height,
10481 scroll_pixel_position,
10482 content_origin,
10483 &gutter_dimensions,
10484 &gutter_hitbox,
10485 &text_hitbox,
10486 relative,
10487 current_selection_head,
10488 window,
10489 cx,
10490 )
10491 } else {
10492 None
10493 };
10494 self.editor.update(cx, |editor, _| {
10495 editor.scroll_manager.set_sticky_header_line_count(
10496 sticky_headers.as_ref().map_or(0, |h| h.lines.len()),
10497 );
10498 });
10499 let indent_guides =
10500 if scroll_pixel_position != preliminary_scroll_pixel_position {
10501 self.layout_indent_guides(
10502 content_origin,
10503 text_hitbox.origin,
10504 start_buffer_row..end_buffer_row,
10505 scroll_pixel_position,
10506 line_height,
10507 &snapshot,
10508 window,
10509 cx,
10510 )
10511 } else {
10512 indent_guides
10513 };
10514
10515 let crease_trailers =
10516 window.with_element_namespace("crease_trailers", |window| {
10517 self.prepaint_crease_trailers(
10518 crease_trailers,
10519 &line_layouts,
10520 line_height,
10521 content_origin,
10522 scroll_pixel_position,
10523 em_width,
10524 window,
10525 cx,
10526 )
10527 });
10528
10529 let (edit_prediction_popover, edit_prediction_popover_origin) = self
10530 .editor
10531 .update(cx, |editor, cx| {
10532 editor.render_edit_prediction_popover(
10533 &text_hitbox.bounds,
10534 content_origin,
10535 right_margin,
10536 &snapshot,
10537 start_row..end_row,
10538 scroll_position.y,
10539 scroll_position.y + height_in_lines,
10540 &line_layouts,
10541 line_height,
10542 scroll_position,
10543 scroll_pixel_position,
10544 newest_selection_head,
10545 editor_width,
10546 style,
10547 window,
10548 cx,
10549 )
10550 })
10551 .unzip();
10552
10553 let mut inline_diagnostics = self.layout_inline_diagnostics(
10554 &line_layouts,
10555 &crease_trailers,
10556 &row_block_types,
10557 content_origin,
10558 scroll_position,
10559 scroll_pixel_position,
10560 edit_prediction_popover_origin,
10561 start_row,
10562 end_row,
10563 line_height,
10564 em_width,
10565 style,
10566 window,
10567 cx,
10568 );
10569
10570 let mut inline_blame_layout = None;
10571 let mut inline_code_actions = None;
10572 if let Some(newest_selection_head) = newest_selection_head {
10573 let display_row = newest_selection_head.row();
10574 if (start_row..end_row).contains(&display_row)
10575 && !row_block_types.contains_key(&display_row)
10576 {
10577 inline_code_actions = self.layout_inline_code_actions(
10578 newest_selection_head,
10579 content_origin,
10580 scroll_position,
10581 scroll_pixel_position,
10582 line_height,
10583 &snapshot,
10584 window,
10585 cx,
10586 );
10587
10588 let line_ix = display_row.minus(start_row) as usize;
10589 if let (Some(row_info), Some(line_layout), Some(crease_trailer)) = (
10590 row_infos.get(line_ix),
10591 line_layouts.get(line_ix),
10592 crease_trailers.get(line_ix),
10593 ) {
10594 let crease_trailer_layout = crease_trailer.as_ref();
10595 if let Some(layout) = self.layout_inline_blame(
10596 display_row,
10597 row_info,
10598 line_layout,
10599 crease_trailer_layout,
10600 em_width,
10601 content_origin,
10602 scroll_position,
10603 scroll_pixel_position,
10604 line_height,
10605 window,
10606 cx,
10607 ) {
10608 inline_blame_layout = Some(layout);
10609 // Blame overrides inline diagnostics
10610 inline_diagnostics.remove(&display_row);
10611 }
10612 } else {
10613 log::error!(
10614 "bug: line_ix {} is out of bounds - row_infos.len(): {}, \
10615 line_layouts.len(): {}, \
10616 crease_trailers.len(): {}",
10617 line_ix,
10618 row_infos.len(),
10619 line_layouts.len(),
10620 crease_trailers.len(),
10621 );
10622 }
10623 }
10624 }
10625
10626 let blamed_display_rows = self.layout_blame_entries(
10627 &row_infos,
10628 em_width,
10629 scroll_position,
10630 line_height,
10631 &gutter_hitbox,
10632 gutter_dimensions.git_blame_entries_width,
10633 window,
10634 cx,
10635 );
10636
10637 let line_elements = self.prepaint_lines(
10638 start_row,
10639 &mut line_layouts,
10640 line_height,
10641 scroll_position,
10642 scroll_pixel_position,
10643 content_origin,
10644 window,
10645 cx,
10646 );
10647
10648 window.with_element_namespace("blocks", |window| {
10649 self.layout_blocks(
10650 &mut blocks,
10651 &hitbox,
10652 &gutter_hitbox,
10653 line_height,
10654 scroll_position,
10655 scroll_pixel_position,
10656 &editor_margins,
10657 window,
10658 cx,
10659 );
10660 self.layout_blocks(
10661 &mut spacer_blocks,
10662 &hitbox,
10663 &gutter_hitbox,
10664 line_height,
10665 scroll_position,
10666 scroll_pixel_position,
10667 &editor_margins,
10668 window,
10669 cx,
10670 );
10671 });
10672
10673 let cursors = self.collect_cursors(&snapshot, cx);
10674 let visible_row_range = start_row..end_row;
10675 let non_visible_cursors = cursors
10676 .iter()
10677 .any(|c| !visible_row_range.contains(&c.0.row()));
10678
10679 let visible_cursors = self.layout_visible_cursors(
10680 &snapshot,
10681 &selections,
10682 &row_block_types,
10683 start_row..end_row,
10684 &line_layouts,
10685 &text_hitbox,
10686 content_origin,
10687 scroll_position,
10688 scroll_pixel_position,
10689 line_height,
10690 em_width,
10691 em_advance,
10692 autoscroll_containing_element,
10693 &redacted_ranges,
10694 window,
10695 cx,
10696 );
10697
10698 let scrollbars_layout = self.layout_scrollbars(
10699 &snapshot,
10700 &scrollbar_layout_information,
10701 content_offset,
10702 scroll_position,
10703 non_visible_cursors,
10704 right_margin,
10705 editor_width,
10706 window,
10707 cx,
10708 );
10709
10710 let gutter_settings = EditorSettings::get_global(cx).gutter;
10711
10712 let context_menu_layout =
10713 if let Some(newest_selection_head) = newest_selection_head {
10714 let newest_selection_point =
10715 newest_selection_head.to_point(&snapshot.display_snapshot);
10716 if (start_row..end_row).contains(&newest_selection_head.row()) {
10717 self.layout_cursor_popovers(
10718 line_height,
10719 &text_hitbox,
10720 content_origin,
10721 right_margin,
10722 start_row,
10723 scroll_pixel_position,
10724 &line_layouts,
10725 newest_selection_head,
10726 newest_selection_point,
10727 style,
10728 window,
10729 cx,
10730 )
10731 } else {
10732 None
10733 }
10734 } else {
10735 None
10736 };
10737
10738 self.layout_gutter_menu(
10739 line_height,
10740 &text_hitbox,
10741 content_origin,
10742 right_margin,
10743 scroll_pixel_position,
10744 gutter_dimensions.width - gutter_dimensions.left_padding,
10745 window,
10746 cx,
10747 );
10748
10749 let test_indicators = if gutter_settings.runnables {
10750 self.layout_run_indicators(
10751 line_height,
10752 start_row..end_row,
10753 &row_infos,
10754 scroll_position,
10755 &gutter_dimensions,
10756 &gutter_hitbox,
10757 &snapshot,
10758 &mut breakpoint_rows,
10759 window,
10760 cx,
10761 )
10762 } else {
10763 Vec::new()
10764 };
10765
10766 let show_breakpoints = snapshot
10767 .show_breakpoints
10768 .unwrap_or(gutter_settings.breakpoints);
10769 let breakpoints = if show_breakpoints {
10770 self.layout_breakpoints(
10771 line_height,
10772 start_row..end_row,
10773 scroll_position,
10774 &gutter_dimensions,
10775 &gutter_hitbox,
10776 &snapshot,
10777 breakpoint_rows,
10778 &row_infos,
10779 window,
10780 cx,
10781 )
10782 } else {
10783 Vec::new()
10784 };
10785
10786 let git_gutter_width = Self::gutter_strip_width(line_height)
10787 + gutter_dimensions
10788 .git_blame_entries_width
10789 .unwrap_or_default();
10790 let available_width = gutter_dimensions.left_padding - git_gutter_width;
10791
10792 let max_line_number_length = self
10793 .editor
10794 .read(cx)
10795 .buffer()
10796 .read(cx)
10797 .snapshot(cx)
10798 .widest_line_number()
10799 .ilog10()
10800 + 1;
10801
10802 let diff_review_button = self
10803 .should_render_diff_review_button(
10804 start_row..end_row,
10805 &row_infos,
10806 &snapshot,
10807 cx,
10808 )
10809 .map(|(display_row, buffer_row)| {
10810 let is_wide = max_line_number_length
10811 >= EditorSettings::get_global(cx).gutter.min_line_number_digits
10812 as u32
10813 && buffer_row.is_some_and(|row| {
10814 (row + 1).ilog10() + 1 == max_line_number_length
10815 })
10816 || gutter_dimensions.right_padding == px(0.);
10817
10818 let button_width = if is_wide {
10819 available_width - px(6.)
10820 } else {
10821 available_width + em_width - px(6.)
10822 };
10823
10824 let button = self.editor.update(cx, |editor, cx| {
10825 editor
10826 .render_diff_review_button(display_row, button_width, cx)
10827 .into_any_element()
10828 });
10829 prepaint_gutter_button(
10830 button,
10831 display_row,
10832 line_height,
10833 &gutter_dimensions,
10834 scroll_position,
10835 &gutter_hitbox,
10836 window,
10837 cx,
10838 )
10839 });
10840
10841 self.layout_signature_help(
10842 &hitbox,
10843 content_origin,
10844 scroll_pixel_position,
10845 newest_selection_head,
10846 start_row,
10847 &line_layouts,
10848 line_height,
10849 em_width,
10850 context_menu_layout,
10851 window,
10852 cx,
10853 );
10854
10855 if !cx.has_active_drag() {
10856 self.layout_hover_popovers(
10857 &snapshot,
10858 &hitbox,
10859 start_row..end_row,
10860 content_origin,
10861 scroll_pixel_position,
10862 &line_layouts,
10863 line_height,
10864 em_width,
10865 context_menu_layout,
10866 window,
10867 cx,
10868 );
10869
10870 self.layout_blame_popover(&snapshot, &hitbox, line_height, window, cx);
10871 }
10872
10873 let mouse_context_menu = self.layout_mouse_context_menu(
10874 &snapshot,
10875 start_row..end_row,
10876 content_origin,
10877 window,
10878 cx,
10879 );
10880
10881 window.with_element_namespace("crease_toggles", |window| {
10882 self.prepaint_crease_toggles(
10883 &mut crease_toggles,
10884 line_height,
10885 &gutter_dimensions,
10886 gutter_settings,
10887 scroll_pixel_position,
10888 &gutter_hitbox,
10889 window,
10890 cx,
10891 )
10892 });
10893
10894 window.with_element_namespace("expand_toggles", |window| {
10895 self.prepaint_expand_toggles(&mut expand_toggles, window, cx)
10896 });
10897
10898 let wrap_guides = self.layout_wrap_guides(
10899 em_advance,
10900 scroll_position,
10901 content_origin,
10902 scrollbars_layout.as_ref(),
10903 vertical_scrollbar_width,
10904 &hitbox,
10905 window,
10906 cx,
10907 );
10908
10909 let minimap = window.with_element_namespace("minimap", |window| {
10910 self.layout_minimap(
10911 &snapshot,
10912 minimap_width,
10913 scroll_position,
10914 &scrollbar_layout_information,
10915 scrollbars_layout.as_ref(),
10916 window,
10917 cx,
10918 )
10919 });
10920
10921 let invisible_symbol_font_size = font_size / 2.;
10922 let whitespace_map = &self
10923 .editor
10924 .read(cx)
10925 .buffer
10926 .read(cx)
10927 .language_settings(cx)
10928 .whitespace_map;
10929
10930 let tab_char = whitespace_map.tab.clone();
10931 let tab_len = tab_char.len();
10932 let tab_invisible = window.text_system().shape_line(
10933 tab_char,
10934 invisible_symbol_font_size,
10935 &[TextRun {
10936 len: tab_len,
10937 font: self.style.text.font(),
10938 color: cx.theme().colors().editor_invisible,
10939 ..Default::default()
10940 }],
10941 None,
10942 );
10943
10944 let space_char = whitespace_map.space.clone();
10945 let space_len = space_char.len();
10946 let space_invisible = window.text_system().shape_line(
10947 space_char,
10948 invisible_symbol_font_size,
10949 &[TextRun {
10950 len: space_len,
10951 font: self.style.text.font(),
10952 color: cx.theme().colors().editor_invisible,
10953 ..Default::default()
10954 }],
10955 None,
10956 );
10957
10958 let mode = snapshot.mode.clone();
10959
10960 let sticky_scroll_header_height = sticky_headers
10961 .as_ref()
10962 .and_then(|headers| headers.lines.last())
10963 .map_or(Pixels::ZERO, |last| last.offset + line_height);
10964
10965 let has_sticky_buffer_header =
10966 sticky_buffer_header.is_some() || sticky_header_excerpt_id.is_some();
10967 let sticky_header_height = if has_sticky_buffer_header {
10968 let full_height = FILE_HEADER_HEIGHT as f32 * line_height;
10969 let display_row = blocks
10970 .iter()
10971 .filter(|block| block.is_buffer_header)
10972 .find_map(|block| {
10973 block.row.filter(|row| row.0 > scroll_position.y as u32)
10974 });
10975 let offset = match display_row {
10976 Some(display_row) => {
10977 let max_row = display_row.0.saturating_sub(FILE_HEADER_HEIGHT);
10978 let offset = (scroll_position.y - max_row as f64).max(0.0);
10979 let slide_up =
10980 Pixels::from(offset * ScrollPixelOffset::from(line_height));
10981
10982 (full_height - slide_up).max(Pixels::ZERO)
10983 }
10984 None => full_height,
10985 };
10986 let header_bottom_padding =
10987 BUFFER_HEADER_PADDING.to_pixels(window.rem_size());
10988 sticky_scroll_header_height + offset - header_bottom_padding
10989 } else {
10990 sticky_scroll_header_height
10991 };
10992
10993 let (diff_hunk_controls, diff_hunk_control_bounds) =
10994 if is_read_only && !self.editor.read(cx).delegate_stage_and_restore {
10995 (vec![], vec![])
10996 } else {
10997 self.layout_diff_hunk_controls(
10998 start_row..end_row,
10999 &row_infos,
11000 &text_hitbox,
11001 current_selection_head,
11002 line_height,
11003 right_margin,
11004 scroll_pixel_position,
11005 sticky_header_height,
11006 &display_hunks,
11007 &highlighted_rows,
11008 self.editor.clone(),
11009 window,
11010 cx,
11011 )
11012 };
11013
11014 let position_map = Rc::new(PositionMap {
11015 size: bounds.size,
11016 visible_row_range,
11017 scroll_position,
11018 scroll_pixel_position,
11019 scroll_max,
11020 line_layouts,
11021 line_height,
11022 em_width,
11023 em_advance,
11024 em_layout_width,
11025 snapshot,
11026 text_align: self.style.text.text_align,
11027 content_width: text_hitbox.size.width,
11028 gutter_hitbox: gutter_hitbox.clone(),
11029 text_hitbox: text_hitbox.clone(),
11030 inline_blame_bounds: inline_blame_layout
11031 .as_ref()
11032 .map(|layout| (layout.bounds, layout.buffer_id, layout.entry.clone())),
11033 display_hunks: display_hunks.clone(),
11034 diff_hunk_control_bounds,
11035 });
11036
11037 self.editor.update(cx, |editor, _| {
11038 editor.last_position_map = Some(position_map.clone())
11039 });
11040
11041 EditorLayout {
11042 mode,
11043 position_map,
11044 visible_display_row_range: start_row..end_row,
11045 wrap_guides,
11046 indent_guides,
11047 hitbox,
11048 gutter_hitbox,
11049 display_hunks,
11050 content_origin,
11051 scrollbars_layout,
11052 minimap,
11053 active_rows,
11054 highlighted_rows,
11055 highlighted_ranges,
11056 highlighted_gutter_ranges,
11057 redacted_ranges,
11058 document_colors,
11059 line_elements,
11060 line_numbers,
11061 blamed_display_rows,
11062 inline_diagnostics,
11063 inline_blame_layout,
11064 inline_code_actions,
11065 blocks,
11066 spacer_blocks,
11067 cursors,
11068 visible_cursors,
11069 selections,
11070 edit_prediction_popover,
11071 diff_hunk_controls,
11072 mouse_context_menu,
11073 test_indicators,
11074 breakpoints,
11075 diff_review_button,
11076 crease_toggles,
11077 crease_trailers,
11078 tab_invisible,
11079 space_invisible,
11080 sticky_buffer_header,
11081 sticky_headers,
11082 expand_toggles,
11083 text_align: self.style.text.text_align,
11084 content_width: text_hitbox.size.width,
11085 }
11086 })
11087 })
11088 })
11089 }
11090
11091 fn paint(
11092 &mut self,
11093 _: Option<&GlobalElementId>,
11094 _inspector_id: Option<&gpui::InspectorElementId>,
11095 bounds: Bounds<gpui::Pixels>,
11096 _: &mut Self::RequestLayoutState,
11097 layout: &mut Self::PrepaintState,
11098 window: &mut Window,
11099 cx: &mut App,
11100 ) {
11101 if !layout.mode.is_minimap() {
11102 let focus_handle = self.editor.focus_handle(cx);
11103 let key_context = self
11104 .editor
11105 .update(cx, |editor, cx| editor.key_context(window, cx));
11106
11107 window.set_key_context(key_context);
11108 window.handle_input(
11109 &focus_handle,
11110 ElementInputHandler::new(bounds, self.editor.clone()),
11111 cx,
11112 );
11113 self.register_actions(window, cx);
11114 self.register_key_listeners(window, cx, layout);
11115 }
11116
11117 let text_style = TextStyleRefinement {
11118 font_size: Some(self.style.text.font_size),
11119 line_height: Some(self.style.text.line_height),
11120 ..Default::default()
11121 };
11122 let rem_size = self.rem_size(cx);
11123 window.with_rem_size(rem_size, |window| {
11124 window.with_text_style(Some(text_style), |window| {
11125 window.with_content_mask(Some(ContentMask { bounds }), |window| {
11126 self.paint_mouse_listeners(layout, window, cx);
11127 self.paint_background(layout, window, cx);
11128
11129 self.paint_indent_guides(layout, window, cx);
11130
11131 if layout.gutter_hitbox.size.width > Pixels::ZERO {
11132 self.paint_blamed_display_rows(layout, window, cx);
11133 self.paint_line_numbers(layout, window, cx);
11134 }
11135
11136 self.paint_text(layout, window, cx);
11137
11138 if !layout.spacer_blocks.is_empty() {
11139 window.with_element_namespace("blocks", |window| {
11140 self.paint_spacer_blocks(layout, window, cx);
11141 });
11142 }
11143
11144 if layout.gutter_hitbox.size.width > Pixels::ZERO {
11145 self.paint_gutter_highlights(layout, window, cx);
11146 self.paint_gutter_indicators(layout, window, cx);
11147 }
11148
11149 if !layout.blocks.is_empty() {
11150 window.with_element_namespace("blocks", |window| {
11151 self.paint_non_spacer_blocks(layout, window, cx);
11152 });
11153 }
11154
11155 window.with_element_namespace("blocks", |window| {
11156 if let Some(mut sticky_header) = layout.sticky_buffer_header.take() {
11157 sticky_header.paint(window, cx)
11158 }
11159 });
11160
11161 self.paint_sticky_headers(layout, window, cx);
11162 self.paint_minimap(layout, window, cx);
11163 self.paint_scrollbars(layout, window, cx);
11164 self.paint_edit_prediction_popover(layout, window, cx);
11165 self.paint_mouse_context_menu(layout, window, cx);
11166 });
11167 })
11168 })
11169 }
11170}
11171
11172pub(super) fn gutter_bounds(
11173 editor_bounds: Bounds<Pixels>,
11174 gutter_dimensions: GutterDimensions,
11175) -> Bounds<Pixels> {
11176 Bounds {
11177 origin: editor_bounds.origin,
11178 size: size(gutter_dimensions.width, editor_bounds.size.height),
11179 }
11180}
11181
11182#[derive(Clone, Copy)]
11183struct ContextMenuLayout {
11184 y_flipped: bool,
11185 bounds: Bounds<Pixels>,
11186}
11187
11188/// Holds information required for layouting the editor scrollbars.
11189struct ScrollbarLayoutInformation {
11190 /// The bounds of the editor area (excluding the content offset).
11191 editor_bounds: Bounds<Pixels>,
11192 /// The available range to scroll within the document.
11193 scroll_range: Size<Pixels>,
11194 /// The space available for one glyph in the editor.
11195 glyph_grid_cell: Size<Pixels>,
11196}
11197
11198impl ScrollbarLayoutInformation {
11199 pub fn new(
11200 editor_bounds: Bounds<Pixels>,
11201 glyph_grid_cell: Size<Pixels>,
11202 document_size: Size<Pixels>,
11203 longest_line_blame_width: Pixels,
11204 settings: &EditorSettings,
11205 scroll_beyond_last_line: ScrollBeyondLastLine,
11206 ) -> Self {
11207 let vertical_overscroll = match scroll_beyond_last_line {
11208 ScrollBeyondLastLine::OnePage => editor_bounds.size.height,
11209 ScrollBeyondLastLine::Off => glyph_grid_cell.height,
11210 ScrollBeyondLastLine::VerticalScrollMargin => {
11211 (1.0 + settings.vertical_scroll_margin) as f32 * glyph_grid_cell.height
11212 }
11213 };
11214
11215 let overscroll = size(longest_line_blame_width, vertical_overscroll);
11216
11217 ScrollbarLayoutInformation {
11218 editor_bounds,
11219 scroll_range: document_size + overscroll,
11220 glyph_grid_cell,
11221 }
11222 }
11223}
11224
11225impl IntoElement for EditorElement {
11226 type Element = Self;
11227
11228 fn into_element(self) -> Self::Element {
11229 self
11230 }
11231}
11232
11233pub struct EditorLayout {
11234 position_map: Rc<PositionMap>,
11235 hitbox: Hitbox,
11236 gutter_hitbox: Hitbox,
11237 content_origin: gpui::Point<Pixels>,
11238 scrollbars_layout: Option<EditorScrollbars>,
11239 minimap: Option<MinimapLayout>,
11240 mode: EditorMode,
11241 wrap_guides: SmallVec<[(Pixels, bool); 2]>,
11242 indent_guides: Option<Vec<IndentGuideLayout>>,
11243 visible_display_row_range: Range<DisplayRow>,
11244 active_rows: BTreeMap<DisplayRow, LineHighlightSpec>,
11245 highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
11246 line_elements: SmallVec<[AnyElement; 1]>,
11247 line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
11248 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
11249 blamed_display_rows: Option<Vec<AnyElement>>,
11250 inline_diagnostics: HashMap<DisplayRow, AnyElement>,
11251 inline_blame_layout: Option<InlineBlameLayout>,
11252 inline_code_actions: Option<AnyElement>,
11253 blocks: Vec<BlockLayout>,
11254 spacer_blocks: Vec<BlockLayout>,
11255 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
11256 highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
11257 redacted_ranges: Vec<Range<DisplayPoint>>,
11258 cursors: Vec<(DisplayPoint, Hsla)>,
11259 visible_cursors: Vec<CursorLayout>,
11260 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
11261 test_indicators: Vec<AnyElement>,
11262 breakpoints: Vec<AnyElement>,
11263 diff_review_button: Option<AnyElement>,
11264 crease_toggles: Vec<Option<AnyElement>>,
11265 expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,
11266 diff_hunk_controls: Vec<AnyElement>,
11267 crease_trailers: Vec<Option<CreaseTrailerLayout>>,
11268 edit_prediction_popover: Option<AnyElement>,
11269 mouse_context_menu: Option<AnyElement>,
11270 tab_invisible: ShapedLine,
11271 space_invisible: ShapedLine,
11272 sticky_buffer_header: Option<AnyElement>,
11273 sticky_headers: Option<StickyHeaders>,
11274 document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
11275 text_align: TextAlign,
11276 content_width: Pixels,
11277}
11278
11279struct StickyHeaders {
11280 lines: Vec<StickyHeaderLine>,
11281 gutter_background: Hsla,
11282 content_background: Hsla,
11283 gutter_right_padding: Pixels,
11284}
11285
11286struct StickyHeaderLine {
11287 row: DisplayRow,
11288 offset: Pixels,
11289 line: Rc<LineWithInvisibles>,
11290 line_number: Option<ShapedLine>,
11291 elements: SmallVec<[AnyElement; 1]>,
11292 available_text_width: Pixels,
11293 hitbox: Hitbox,
11294}
11295
11296impl EditorLayout {
11297 fn line_end_overshoot(&self) -> Pixels {
11298 0.15 * self.position_map.line_height
11299 }
11300}
11301
11302impl StickyHeaders {
11303 fn paint(
11304 &mut self,
11305 layout: &mut EditorLayout,
11306 whitespace_setting: ShowWhitespaceSetting,
11307 window: &mut Window,
11308 cx: &mut App,
11309 ) {
11310 let line_height = layout.position_map.line_height;
11311
11312 for line in self.lines.iter_mut().rev() {
11313 window.paint_layer(
11314 Bounds::new(
11315 layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
11316 size(line.hitbox.size.width, line_height),
11317 ),
11318 |window| {
11319 let gutter_bounds = Bounds::new(
11320 layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
11321 size(layout.gutter_hitbox.size.width, line_height),
11322 );
11323 window.paint_quad(fill(gutter_bounds, self.gutter_background));
11324
11325 let text_bounds = Bounds::new(
11326 layout.position_map.text_hitbox.origin + point(Pixels::ZERO, line.offset),
11327 size(line.available_text_width, line_height),
11328 );
11329 window.paint_quad(fill(text_bounds, self.content_background));
11330
11331 if line.hitbox.is_hovered(window) {
11332 let hover_overlay = cx.theme().colors().panel_overlay_hover;
11333 window.paint_quad(fill(gutter_bounds, hover_overlay));
11334 window.paint_quad(fill(text_bounds, hover_overlay));
11335 }
11336
11337 line.paint(
11338 layout,
11339 self.gutter_right_padding,
11340 line.available_text_width,
11341 layout.content_origin,
11342 line_height,
11343 whitespace_setting,
11344 window,
11345 cx,
11346 );
11347 },
11348 );
11349
11350 window.set_cursor_style(CursorStyle::IBeam, &line.hitbox);
11351 }
11352 }
11353}
11354
11355impl StickyHeaderLine {
11356 fn new(
11357 row: DisplayRow,
11358 offset: Pixels,
11359 mut line: LineWithInvisibles,
11360 line_number: Option<ShapedLine>,
11361 line_height: Pixels,
11362 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
11363 content_origin: gpui::Point<Pixels>,
11364 gutter_hitbox: &Hitbox,
11365 text_hitbox: &Hitbox,
11366 window: &mut Window,
11367 cx: &mut App,
11368 ) -> Self {
11369 let mut elements = SmallVec::<[AnyElement; 1]>::new();
11370 line.prepaint_with_custom_offset(
11371 line_height,
11372 scroll_pixel_position,
11373 content_origin,
11374 offset,
11375 &mut elements,
11376 window,
11377 cx,
11378 );
11379
11380 let hitbox_bounds = Bounds::new(
11381 gutter_hitbox.origin + point(Pixels::ZERO, offset),
11382 size(text_hitbox.right() - gutter_hitbox.left(), line_height),
11383 );
11384 let available_text_width =
11385 (hitbox_bounds.size.width - gutter_hitbox.size.width).max(Pixels::ZERO);
11386
11387 Self {
11388 row,
11389 offset,
11390 line: Rc::new(line),
11391 line_number,
11392 elements,
11393 available_text_width,
11394 hitbox: window.insert_hitbox(hitbox_bounds, HitboxBehavior::BlockMouseExceptScroll),
11395 }
11396 }
11397
11398 fn paint(
11399 &mut self,
11400 layout: &EditorLayout,
11401 gutter_right_padding: Pixels,
11402 available_text_width: Pixels,
11403 content_origin: gpui::Point<Pixels>,
11404 line_height: Pixels,
11405 whitespace_setting: ShowWhitespaceSetting,
11406 window: &mut Window,
11407 cx: &mut App,
11408 ) {
11409 window.with_content_mask(
11410 Some(ContentMask {
11411 bounds: Bounds::new(
11412 layout.position_map.text_hitbox.bounds.origin
11413 + point(Pixels::ZERO, self.offset),
11414 size(available_text_width, line_height),
11415 ),
11416 }),
11417 |window| {
11418 self.line.draw_with_custom_offset(
11419 layout,
11420 self.row,
11421 content_origin,
11422 self.offset,
11423 whitespace_setting,
11424 &[],
11425 window,
11426 cx,
11427 );
11428 for element in &mut self.elements {
11429 element.paint(window, cx);
11430 }
11431 },
11432 );
11433
11434 if let Some(line_number) = &self.line_number {
11435 let gutter_origin = layout.gutter_hitbox.origin + point(Pixels::ZERO, self.offset);
11436 let gutter_width = layout.gutter_hitbox.size.width;
11437 let origin = point(
11438 gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,
11439 gutter_origin.y,
11440 );
11441 line_number
11442 .paint(origin, line_height, TextAlign::Left, None, window, cx)
11443 .log_err();
11444 }
11445 }
11446}
11447
11448#[derive(Debug)]
11449struct LineNumberSegment {
11450 shaped_line: ShapedLine,
11451 hitbox: Option<Hitbox>,
11452}
11453
11454#[derive(Debug)]
11455struct LineNumberLayout {
11456 segments: SmallVec<[LineNumberSegment; 1]>,
11457}
11458
11459struct ColoredRange<T> {
11460 start: T,
11461 end: T,
11462 color: Hsla,
11463}
11464
11465impl Along for ScrollbarAxes {
11466 type Unit = bool;
11467
11468 fn along(&self, axis: ScrollbarAxis) -> Self::Unit {
11469 match axis {
11470 ScrollbarAxis::Horizontal => self.horizontal,
11471 ScrollbarAxis::Vertical => self.vertical,
11472 }
11473 }
11474
11475 fn apply_along(&self, axis: ScrollbarAxis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self {
11476 match axis {
11477 ScrollbarAxis::Horizontal => ScrollbarAxes {
11478 horizontal: f(self.horizontal),
11479 vertical: self.vertical,
11480 },
11481 ScrollbarAxis::Vertical => ScrollbarAxes {
11482 horizontal: self.horizontal,
11483 vertical: f(self.vertical),
11484 },
11485 }
11486 }
11487}
11488
11489#[derive(Clone)]
11490struct EditorScrollbars {
11491 pub vertical: Option<ScrollbarLayout>,
11492 pub horizontal: Option<ScrollbarLayout>,
11493 pub visible: bool,
11494}
11495
11496impl EditorScrollbars {
11497 pub fn from_scrollbar_axes(
11498 show_scrollbar: ScrollbarAxes,
11499 layout_information: &ScrollbarLayoutInformation,
11500 content_offset: gpui::Point<Pixels>,
11501 scroll_position: gpui::Point<f64>,
11502 scrollbar_width: Pixels,
11503 right_margin: Pixels,
11504 editor_width: Pixels,
11505 show_scrollbars: bool,
11506 scrollbar_state: Option<&ActiveScrollbarState>,
11507 window: &mut Window,
11508 ) -> Self {
11509 let ScrollbarLayoutInformation {
11510 editor_bounds,
11511 scroll_range,
11512 glyph_grid_cell,
11513 } = layout_information;
11514
11515 let viewport_size = size(editor_width, editor_bounds.size.height);
11516
11517 let scrollbar_bounds_for = |axis: ScrollbarAxis| match axis {
11518 ScrollbarAxis::Horizontal => Bounds::from_corner_and_size(
11519 Corner::BottomLeft,
11520 editor_bounds.bottom_left(),
11521 size(
11522 // The horizontal viewport size differs from the space available for the
11523 // horizontal scrollbar, so we have to manually stitch it together here.
11524 editor_bounds.size.width - right_margin,
11525 scrollbar_width,
11526 ),
11527 ),
11528 ScrollbarAxis::Vertical => Bounds::from_corner_and_size(
11529 Corner::TopRight,
11530 editor_bounds.top_right(),
11531 size(scrollbar_width, viewport_size.height),
11532 ),
11533 };
11534
11535 let mut create_scrollbar_layout = |axis| {
11536 let viewport_size = viewport_size.along(axis);
11537 let scroll_range = scroll_range.along(axis);
11538
11539 // We always want a vertical scrollbar track for scrollbar diagnostic visibility.
11540 (show_scrollbar.along(axis)
11541 && (axis == ScrollbarAxis::Vertical || scroll_range > viewport_size))
11542 .then(|| {
11543 ScrollbarLayout::new(
11544 window.insert_hitbox(scrollbar_bounds_for(axis), HitboxBehavior::Normal),
11545 viewport_size,
11546 scroll_range,
11547 glyph_grid_cell.along(axis),
11548 content_offset.along(axis),
11549 scroll_position.along(axis),
11550 show_scrollbars,
11551 axis,
11552 )
11553 .with_thumb_state(
11554 scrollbar_state.and_then(|state| state.thumb_state_for_axis(axis)),
11555 )
11556 })
11557 };
11558
11559 Self {
11560 vertical: create_scrollbar_layout(ScrollbarAxis::Vertical),
11561 horizontal: create_scrollbar_layout(ScrollbarAxis::Horizontal),
11562 visible: show_scrollbars,
11563 }
11564 }
11565
11566 pub fn iter_scrollbars(&self) -> impl Iterator<Item = (&ScrollbarLayout, ScrollbarAxis)> + '_ {
11567 [
11568 (&self.vertical, ScrollbarAxis::Vertical),
11569 (&self.horizontal, ScrollbarAxis::Horizontal),
11570 ]
11571 .into_iter()
11572 .filter_map(|(scrollbar, axis)| scrollbar.as_ref().map(|s| (s, axis)))
11573 }
11574
11575 /// Returns the currently hovered scrollbar axis, if any.
11576 pub fn get_hovered_axis(&self, window: &Window) -> Option<(&ScrollbarLayout, ScrollbarAxis)> {
11577 self.iter_scrollbars()
11578 .find(|s| s.0.hitbox.is_hovered(window))
11579 }
11580}
11581
11582#[derive(Clone)]
11583struct ScrollbarLayout {
11584 hitbox: Hitbox,
11585 visible_range: Range<ScrollOffset>,
11586 text_unit_size: Pixels,
11587 thumb_bounds: Option<Bounds<Pixels>>,
11588 thumb_state: ScrollbarThumbState,
11589}
11590
11591impl ScrollbarLayout {
11592 const BORDER_WIDTH: Pixels = px(1.0);
11593 const LINE_MARKER_HEIGHT: Pixels = px(2.0);
11594 const MIN_MARKER_HEIGHT: Pixels = px(5.0);
11595 const MIN_THUMB_SIZE: Pixels = px(25.0);
11596
11597 fn new(
11598 scrollbar_track_hitbox: Hitbox,
11599 viewport_size: Pixels,
11600 scroll_range: Pixels,
11601 glyph_space: Pixels,
11602 content_offset: Pixels,
11603 scroll_position: ScrollOffset,
11604 show_thumb: bool,
11605 axis: ScrollbarAxis,
11606 ) -> Self {
11607 let track_bounds = scrollbar_track_hitbox.bounds;
11608 // The length of the track available to the scrollbar thumb. We deliberately
11609 // exclude the content size here so that the thumb aligns with the content.
11610 let track_length = track_bounds.size.along(axis) - content_offset;
11611
11612 Self::new_with_hitbox_and_track_length(
11613 scrollbar_track_hitbox,
11614 track_length,
11615 viewport_size,
11616 scroll_range.into(),
11617 glyph_space,
11618 content_offset.into(),
11619 scroll_position,
11620 show_thumb,
11621 axis,
11622 )
11623 }
11624
11625 fn for_minimap(
11626 minimap_track_hitbox: Hitbox,
11627 visible_lines: f64,
11628 total_editor_lines: f64,
11629 minimap_line_height: Pixels,
11630 scroll_position: ScrollOffset,
11631 minimap_scroll_top: ScrollOffset,
11632 show_thumb: bool,
11633 ) -> Self {
11634 // The scrollbar thumb size is calculated as
11635 // (visible_content/total_content) Γ scrollbar_track_length.
11636 //
11637 // For the minimap's thumb layout, we leverage this by setting the
11638 // scrollbar track length to the entire document size (using minimap line
11639 // height). This creates a thumb that exactly represents the editor
11640 // viewport scaled to minimap proportions.
11641 //
11642 // We adjust the thumb position relative to `minimap_scroll_top` to
11643 // accommodate for the deliberately oversized track.
11644 //
11645 // This approach ensures that the minimap thumb accurately reflects the
11646 // editor's current scroll position whilst nicely synchronizing the minimap
11647 // thumb and scrollbar thumb.
11648 let scroll_range = total_editor_lines * f64::from(minimap_line_height);
11649 let viewport_size = visible_lines * f64::from(minimap_line_height);
11650
11651 let track_top_offset = -minimap_scroll_top * f64::from(minimap_line_height);
11652
11653 Self::new_with_hitbox_and_track_length(
11654 minimap_track_hitbox,
11655 Pixels::from(scroll_range),
11656 Pixels::from(viewport_size),
11657 scroll_range,
11658 minimap_line_height,
11659 track_top_offset,
11660 scroll_position,
11661 show_thumb,
11662 ScrollbarAxis::Vertical,
11663 )
11664 }
11665
11666 fn new_with_hitbox_and_track_length(
11667 scrollbar_track_hitbox: Hitbox,
11668 track_length: Pixels,
11669 viewport_size: Pixels,
11670 scroll_range: f64,
11671 glyph_space: Pixels,
11672 content_offset: ScrollOffset,
11673 scroll_position: ScrollOffset,
11674 show_thumb: bool,
11675 axis: ScrollbarAxis,
11676 ) -> Self {
11677 let text_units_per_page = viewport_size.to_f64() / glyph_space.to_f64();
11678 let visible_range = scroll_position..scroll_position + text_units_per_page;
11679 let total_text_units = scroll_range / glyph_space.to_f64();
11680
11681 let thumb_percentage = text_units_per_page / total_text_units;
11682 let thumb_size = Pixels::from(ScrollOffset::from(track_length) * thumb_percentage)
11683 .max(ScrollbarLayout::MIN_THUMB_SIZE)
11684 .min(track_length);
11685
11686 let text_unit_divisor = (total_text_units - text_units_per_page).max(0.);
11687
11688 let content_larger_than_viewport = text_unit_divisor > 0.;
11689
11690 let text_unit_size = if content_larger_than_viewport {
11691 Pixels::from(ScrollOffset::from(track_length - thumb_size) / text_unit_divisor)
11692 } else {
11693 glyph_space
11694 };
11695
11696 let thumb_bounds = (show_thumb && content_larger_than_viewport).then(|| {
11697 Self::thumb_bounds(
11698 &scrollbar_track_hitbox,
11699 content_offset,
11700 visible_range.start,
11701 text_unit_size,
11702 thumb_size,
11703 axis,
11704 )
11705 });
11706
11707 ScrollbarLayout {
11708 hitbox: scrollbar_track_hitbox,
11709 visible_range,
11710 text_unit_size,
11711 thumb_bounds,
11712 thumb_state: Default::default(),
11713 }
11714 }
11715
11716 fn with_thumb_state(self, thumb_state: Option<ScrollbarThumbState>) -> Self {
11717 if let Some(thumb_state) = thumb_state {
11718 Self {
11719 thumb_state,
11720 ..self
11721 }
11722 } else {
11723 self
11724 }
11725 }
11726
11727 fn thumb_bounds(
11728 scrollbar_track: &Hitbox,
11729 content_offset: f64,
11730 visible_range_start: f64,
11731 text_unit_size: Pixels,
11732 thumb_size: Pixels,
11733 axis: ScrollbarAxis,
11734 ) -> Bounds<Pixels> {
11735 let thumb_origin = scrollbar_track.origin.apply_along(axis, |origin| {
11736 origin
11737 + Pixels::from(
11738 content_offset + visible_range_start * ScrollOffset::from(text_unit_size),
11739 )
11740 });
11741 Bounds::new(
11742 thumb_origin,
11743 scrollbar_track.size.apply_along(axis, |_| thumb_size),
11744 )
11745 }
11746
11747 fn thumb_hovered(&self, position: &gpui::Point<Pixels>) -> bool {
11748 self.thumb_bounds
11749 .is_some_and(|bounds| bounds.contains(position))
11750 }
11751
11752 fn marker_quads_for_ranges(
11753 &self,
11754 row_ranges: impl IntoIterator<Item = ColoredRange<DisplayRow>>,
11755 column: Option<usize>,
11756 ) -> Vec<PaintQuad> {
11757 struct MinMax {
11758 min: Pixels,
11759 max: Pixels,
11760 }
11761 let (x_range, height_limit) = if let Some(column) = column {
11762 let column_width = ((self.hitbox.size.width - Self::BORDER_WIDTH) / 3.0).floor();
11763 let start = Self::BORDER_WIDTH + (column as f32 * column_width);
11764 let end = start + column_width;
11765 (
11766 Range { start, end },
11767 MinMax {
11768 min: Self::MIN_MARKER_HEIGHT,
11769 max: px(f32::MAX),
11770 },
11771 )
11772 } else {
11773 (
11774 Range {
11775 start: Self::BORDER_WIDTH,
11776 end: self.hitbox.size.width,
11777 },
11778 MinMax {
11779 min: Self::LINE_MARKER_HEIGHT,
11780 max: Self::LINE_MARKER_HEIGHT,
11781 },
11782 )
11783 };
11784
11785 let row_to_y = |row: DisplayRow| row.as_f64() as f32 * self.text_unit_size;
11786 let mut pixel_ranges = row_ranges
11787 .into_iter()
11788 .map(|range| {
11789 let start_y = row_to_y(range.start);
11790 let end_y = row_to_y(range.end)
11791 + self
11792 .text_unit_size
11793 .max(height_limit.min)
11794 .min(height_limit.max);
11795 ColoredRange {
11796 start: start_y,
11797 end: end_y,
11798 color: range.color,
11799 }
11800 })
11801 .peekable();
11802
11803 let mut quads = Vec::new();
11804 while let Some(mut pixel_range) = pixel_ranges.next() {
11805 while let Some(next_pixel_range) = pixel_ranges.peek() {
11806 if pixel_range.end >= next_pixel_range.start - px(1.0)
11807 && pixel_range.color == next_pixel_range.color
11808 {
11809 pixel_range.end = next_pixel_range.end.max(pixel_range.end);
11810 pixel_ranges.next();
11811 } else {
11812 break;
11813 }
11814 }
11815
11816 let bounds = Bounds::from_corners(
11817 point(x_range.start, pixel_range.start),
11818 point(x_range.end, pixel_range.end),
11819 );
11820 quads.push(quad(
11821 bounds,
11822 Corners::default(),
11823 pixel_range.color,
11824 Edges::default(),
11825 Hsla::transparent_black(),
11826 BorderStyle::default(),
11827 ));
11828 }
11829
11830 quads
11831 }
11832}
11833
11834struct MinimapLayout {
11835 pub minimap: AnyElement,
11836 pub thumb_layout: ScrollbarLayout,
11837 pub minimap_scroll_top: ScrollOffset,
11838 pub minimap_line_height: Pixels,
11839 pub thumb_border_style: MinimapThumbBorder,
11840 pub max_scroll_top: ScrollOffset,
11841}
11842
11843impl MinimapLayout {
11844 /// The minimum width of the minimap in columns. If the minimap is smaller than this, it will be hidden.
11845 const MINIMAP_MIN_WIDTH_COLUMNS: f32 = 20.;
11846 /// The minimap width as a percentage of the editor width.
11847 const MINIMAP_WIDTH_PCT: f32 = 0.15;
11848 /// Calculates the scroll top offset the minimap editor has to have based on the
11849 /// current scroll progress.
11850 fn calculate_minimap_top_offset(
11851 document_lines: f64,
11852 visible_editor_lines: f64,
11853 visible_minimap_lines: f64,
11854 scroll_position: f64,
11855 ) -> ScrollOffset {
11856 let non_visible_document_lines = (document_lines - visible_editor_lines).max(0.);
11857 if non_visible_document_lines == 0. {
11858 0.
11859 } else {
11860 let scroll_percentage = (scroll_position / non_visible_document_lines).clamp(0., 1.);
11861 scroll_percentage * (document_lines - visible_minimap_lines).max(0.)
11862 }
11863 }
11864}
11865
11866struct CreaseTrailerLayout {
11867 element: AnyElement,
11868 bounds: Bounds<Pixels>,
11869}
11870
11871pub(crate) struct PositionMap {
11872 pub size: Size<Pixels>,
11873 pub line_height: Pixels,
11874 pub scroll_position: gpui::Point<ScrollOffset>,
11875 pub scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
11876 pub scroll_max: gpui::Point<ScrollOffset>,
11877 pub em_width: Pixels,
11878 pub em_advance: Pixels,
11879 pub em_layout_width: Pixels,
11880 pub visible_row_range: Range<DisplayRow>,
11881 pub line_layouts: Vec<LineWithInvisibles>,
11882 pub snapshot: EditorSnapshot,
11883 pub text_align: TextAlign,
11884 pub content_width: Pixels,
11885 pub text_hitbox: Hitbox,
11886 pub gutter_hitbox: Hitbox,
11887 pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,
11888 pub display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
11889 pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds<Pixels>)>,
11890}
11891
11892#[derive(Debug, Copy, Clone)]
11893pub struct PointForPosition {
11894 pub previous_valid: DisplayPoint,
11895 pub next_valid: DisplayPoint,
11896 pub exact_unclipped: DisplayPoint,
11897 pub column_overshoot_after_line_end: u32,
11898}
11899
11900impl PointForPosition {
11901 pub fn as_valid(&self) -> Option<DisplayPoint> {
11902 if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
11903 Some(self.previous_valid)
11904 } else {
11905 None
11906 }
11907 }
11908
11909 pub fn intersects_selection(&self, selection: &Selection<DisplayPoint>) -> bool {
11910 let Some(valid_point) = self.as_valid() else {
11911 return false;
11912 };
11913 let range = selection.range();
11914
11915 let candidate_row = valid_point.row();
11916 let candidate_col = valid_point.column();
11917
11918 let start_row = range.start.row();
11919 let start_col = range.start.column();
11920 let end_row = range.end.row();
11921 let end_col = range.end.column();
11922
11923 if candidate_row < start_row || candidate_row > end_row {
11924 false
11925 } else if start_row == end_row {
11926 candidate_col >= start_col && candidate_col < end_col
11927 } else if candidate_row == start_row {
11928 candidate_col >= start_col
11929 } else if candidate_row == end_row {
11930 candidate_col < end_col
11931 } else {
11932 true
11933 }
11934 }
11935}
11936
11937impl PositionMap {
11938 pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
11939 let text_bounds = self.text_hitbox.bounds;
11940 let scroll_position = self.snapshot.scroll_position();
11941 let position = position - text_bounds.origin;
11942 let y = position.y.max(px(0.)).min(self.size.height);
11943 let x = position.x + (scroll_position.x as f32 * self.em_layout_width);
11944 let row = ((y / self.line_height) as f64 + scroll_position.y) as u32;
11945
11946 let (column, x_overshoot_after_line_end) = if let Some(line) = self
11947 .line_layouts
11948 .get(row as usize - scroll_position.y as usize)
11949 {
11950 let alignment_offset = line.alignment_offset(self.text_align, self.content_width);
11951 let x_relative_to_text = x - alignment_offset;
11952 if let Some(ix) = line.index_for_x(x_relative_to_text) {
11953 (ix as u32, px(0.))
11954 } else {
11955 (line.len as u32, px(0.).max(x_relative_to_text - line.width))
11956 }
11957 } else {
11958 (0, x)
11959 };
11960
11961 let mut exact_unclipped = DisplayPoint::new(DisplayRow(row), column);
11962 let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
11963 let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
11964
11965 let column_overshoot_after_line_end =
11966 (x_overshoot_after_line_end / self.em_layout_width) as u32;
11967 *exact_unclipped.column_mut() += column_overshoot_after_line_end;
11968 PointForPosition {
11969 previous_valid,
11970 next_valid,
11971 exact_unclipped,
11972 column_overshoot_after_line_end,
11973 }
11974 }
11975
11976 fn point_for_position_on_line(
11977 &self,
11978 position: gpui::Point<Pixels>,
11979 row: DisplayRow,
11980 line: &LineWithInvisibles,
11981 ) -> PointForPosition {
11982 let text_bounds = self.text_hitbox.bounds;
11983 let scroll_position = self.snapshot.scroll_position();
11984 let position = position - text_bounds.origin;
11985 let x = position.x + (scroll_position.x as f32 * self.em_layout_width);
11986
11987 let alignment_offset = line.alignment_offset(self.text_align, self.content_width);
11988 let x_relative_to_text = x - alignment_offset;
11989 let (column, x_overshoot_after_line_end) =
11990 if let Some(ix) = line.index_for_x(x_relative_to_text) {
11991 (ix as u32, px(0.))
11992 } else {
11993 (line.len as u32, px(0.).max(x_relative_to_text - line.width))
11994 };
11995
11996 let mut exact_unclipped = DisplayPoint::new(row, column);
11997 let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
11998 let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
11999
12000 let column_overshoot_after_line_end =
12001 (x_overshoot_after_line_end / self.em_layout_width) as u32;
12002 *exact_unclipped.column_mut() += column_overshoot_after_line_end;
12003 PointForPosition {
12004 previous_valid,
12005 next_valid,
12006 exact_unclipped,
12007 column_overshoot_after_line_end,
12008 }
12009 }
12010}
12011
12012pub(crate) struct BlockLayout {
12013 pub(crate) id: BlockId,
12014 pub(crate) x_offset: Pixels,
12015 pub(crate) row: Option<DisplayRow>,
12016 pub(crate) element: AnyElement,
12017 pub(crate) available_space: Size<AvailableSpace>,
12018 pub(crate) style: BlockStyle,
12019 pub(crate) overlaps_gutter: bool,
12020 pub(crate) is_buffer_header: bool,
12021}
12022
12023pub fn layout_line(
12024 row: DisplayRow,
12025 snapshot: &EditorSnapshot,
12026 style: &EditorStyle,
12027 text_width: Pixels,
12028 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
12029 window: &mut Window,
12030 cx: &mut App,
12031) -> LineWithInvisibles {
12032 let use_tree_sitter =
12033 !snapshot.semantic_tokens_enabled || snapshot.use_tree_sitter_for_syntax(row, cx);
12034 let language_aware = LanguageAwareStyling {
12035 tree_sitter: use_tree_sitter,
12036 diagnostics: true,
12037 };
12038 let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), language_aware, style);
12039 LineWithInvisibles::from_chunks(
12040 chunks,
12041 style,
12042 MAX_LINE_LEN,
12043 1,
12044 &snapshot.mode,
12045 text_width,
12046 is_row_soft_wrapped,
12047 &[],
12048 window,
12049 cx,
12050 )
12051 .pop()
12052 .unwrap()
12053}
12054
12055#[derive(Debug, Clone)]
12056pub struct IndentGuideLayout {
12057 origin: gpui::Point<Pixels>,
12058 length: Pixels,
12059 single_indent_width: Pixels,
12060 display_row_range: Range<DisplayRow>,
12061 depth: u32,
12062 active: bool,
12063 settings: IndentGuideSettings,
12064}
12065
12066pub struct CursorLayout {
12067 origin: gpui::Point<Pixels>,
12068 block_width: Pixels,
12069 line_height: Pixels,
12070 color: Hsla,
12071 shape: CursorShape,
12072 block_text: Option<ShapedLine>,
12073 cursor_name: Option<AnyElement>,
12074}
12075
12076#[derive(Debug)]
12077pub struct CursorName {
12078 string: SharedString,
12079 color: Hsla,
12080 is_top_row: bool,
12081}
12082
12083impl CursorLayout {
12084 pub fn new(
12085 origin: gpui::Point<Pixels>,
12086 block_width: Pixels,
12087 line_height: Pixels,
12088 color: Hsla,
12089 shape: CursorShape,
12090 block_text: Option<ShapedLine>,
12091 ) -> CursorLayout {
12092 CursorLayout {
12093 origin,
12094 block_width,
12095 line_height,
12096 color,
12097 shape,
12098 block_text,
12099 cursor_name: None,
12100 }
12101 }
12102
12103 pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
12104 Bounds {
12105 origin: self.origin + origin,
12106 size: size(self.block_width, self.line_height),
12107 }
12108 }
12109
12110 fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
12111 match self.shape {
12112 CursorShape::Bar => Bounds {
12113 origin: self.origin + origin,
12114 size: size(px(2.0), self.line_height),
12115 },
12116 CursorShape::Block | CursorShape::Hollow => Bounds {
12117 origin: self.origin + origin,
12118 size: size(self.block_width, self.line_height),
12119 },
12120 CursorShape::Underline => Bounds {
12121 origin: self.origin
12122 + origin
12123 + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)),
12124 size: size(self.block_width, px(2.0)),
12125 },
12126 }
12127 }
12128
12129 pub fn layout(
12130 &mut self,
12131 origin: gpui::Point<Pixels>,
12132 cursor_name: Option<CursorName>,
12133 window: &mut Window,
12134 cx: &mut App,
12135 ) {
12136 if let Some(cursor_name) = cursor_name {
12137 let bounds = self.bounds(origin);
12138 let text_size = self.line_height / 1.5;
12139
12140 let name_origin = if cursor_name.is_top_row {
12141 point(bounds.right() - px(1.), bounds.top())
12142 } else {
12143 match self.shape {
12144 CursorShape::Bar => point(
12145 bounds.right() - px(2.),
12146 bounds.top() - text_size / 2. - px(1.),
12147 ),
12148 _ => point(
12149 bounds.right() - px(1.),
12150 bounds.top() - text_size / 2. - px(1.),
12151 ),
12152 }
12153 };
12154 let mut name_element = div()
12155 .bg(self.color)
12156 .text_size(text_size)
12157 .px_0p5()
12158 .line_height(text_size + px(2.))
12159 .text_color(cursor_name.color)
12160 .child(cursor_name.string)
12161 .into_any_element();
12162
12163 name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx);
12164
12165 self.cursor_name = Some(name_element);
12166 }
12167 }
12168
12169 pub fn paint(&mut self, origin: gpui::Point<Pixels>, window: &mut Window, cx: &mut App) {
12170 let bounds = self.bounds(origin);
12171
12172 //Draw background or border quad
12173 let cursor = if matches!(self.shape, CursorShape::Hollow) {
12174 outline(bounds, self.color, BorderStyle::Solid)
12175 } else {
12176 fill(bounds, self.color)
12177 };
12178
12179 if let Some(name) = &mut self.cursor_name {
12180 name.paint(window, cx);
12181 }
12182
12183 window.paint_quad(cursor);
12184
12185 if let Some(block_text) = &self.block_text {
12186 block_text
12187 .paint(
12188 self.origin + origin,
12189 self.line_height,
12190 TextAlign::Left,
12191 None,
12192 window,
12193 cx,
12194 )
12195 .log_err();
12196 }
12197 }
12198
12199 pub fn shape(&self) -> CursorShape {
12200 self.shape
12201 }
12202}
12203
12204#[derive(Debug)]
12205pub struct HighlightedRange {
12206 pub start_y: Pixels,
12207 pub line_height: Pixels,
12208 pub lines: Vec<HighlightedRangeLine>,
12209 pub color: Hsla,
12210 pub corner_radius: Pixels,
12211}
12212
12213#[derive(Debug)]
12214pub struct HighlightedRangeLine {
12215 pub start_x: Pixels,
12216 pub end_x: Pixels,
12217}
12218
12219impl HighlightedRange {
12220 pub fn paint(&self, fill: bool, bounds: Bounds<Pixels>, window: &mut Window) {
12221 if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
12222 self.paint_lines(self.start_y, &self.lines[0..1], fill, bounds, window);
12223 self.paint_lines(
12224 self.start_y + self.line_height,
12225 &self.lines[1..],
12226 fill,
12227 bounds,
12228 window,
12229 );
12230 } else {
12231 self.paint_lines(self.start_y, &self.lines, fill, bounds, window);
12232 }
12233 }
12234
12235 fn paint_lines(
12236 &self,
12237 start_y: Pixels,
12238 lines: &[HighlightedRangeLine],
12239 fill: bool,
12240 _bounds: Bounds<Pixels>,
12241 window: &mut Window,
12242 ) {
12243 if lines.is_empty() {
12244 return;
12245 }
12246
12247 let first_line = lines.first().unwrap();
12248 let last_line = lines.last().unwrap();
12249
12250 let first_top_left = point(first_line.start_x, start_y);
12251 let first_top_right = point(first_line.end_x, start_y);
12252
12253 let curve_height = point(Pixels::ZERO, self.corner_radius);
12254 let curve_width = |start_x: Pixels, end_x: Pixels| {
12255 let max = (end_x - start_x) / 2.;
12256 let width = if max < self.corner_radius {
12257 max
12258 } else {
12259 self.corner_radius
12260 };
12261
12262 point(width, Pixels::ZERO)
12263 };
12264
12265 let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
12266 let mut builder = if fill {
12267 gpui::PathBuilder::fill()
12268 } else {
12269 gpui::PathBuilder::stroke(px(1.))
12270 };
12271 builder.move_to(first_top_right - top_curve_width);
12272 builder.curve_to(first_top_right + curve_height, first_top_right);
12273
12274 let mut iter = lines.iter().enumerate().peekable();
12275 while let Some((ix, line)) = iter.next() {
12276 let bottom_right = point(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
12277
12278 if let Some((_, next_line)) = iter.peek() {
12279 let next_top_right = point(next_line.end_x, bottom_right.y);
12280
12281 match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() {
12282 Ordering::Equal => {
12283 builder.line_to(bottom_right);
12284 }
12285 Ordering::Less => {
12286 let curve_width = curve_width(next_top_right.x, bottom_right.x);
12287 builder.line_to(bottom_right - curve_height);
12288 if self.corner_radius > Pixels::ZERO {
12289 builder.curve_to(bottom_right - curve_width, bottom_right);
12290 }
12291 builder.line_to(next_top_right + curve_width);
12292 if self.corner_radius > Pixels::ZERO {
12293 builder.curve_to(next_top_right + curve_height, next_top_right);
12294 }
12295 }
12296 Ordering::Greater => {
12297 let curve_width = curve_width(bottom_right.x, next_top_right.x);
12298 builder.line_to(bottom_right - curve_height);
12299 if self.corner_radius > Pixels::ZERO {
12300 builder.curve_to(bottom_right + curve_width, bottom_right);
12301 }
12302 builder.line_to(next_top_right - curve_width);
12303 if self.corner_radius > Pixels::ZERO {
12304 builder.curve_to(next_top_right + curve_height, next_top_right);
12305 }
12306 }
12307 }
12308 } else {
12309 let curve_width = curve_width(line.start_x, line.end_x);
12310 builder.line_to(bottom_right - curve_height);
12311 if self.corner_radius > Pixels::ZERO {
12312 builder.curve_to(bottom_right - curve_width, bottom_right);
12313 }
12314
12315 let bottom_left = point(line.start_x, bottom_right.y);
12316 builder.line_to(bottom_left + curve_width);
12317 if self.corner_radius > Pixels::ZERO {
12318 builder.curve_to(bottom_left - curve_height, bottom_left);
12319 }
12320 }
12321 }
12322
12323 if first_line.start_x > last_line.start_x {
12324 let curve_width = curve_width(last_line.start_x, first_line.start_x);
12325 let second_top_left = point(last_line.start_x, start_y + self.line_height);
12326 builder.line_to(second_top_left + curve_height);
12327 if self.corner_radius > Pixels::ZERO {
12328 builder.curve_to(second_top_left + curve_width, second_top_left);
12329 }
12330 let first_bottom_left = point(first_line.start_x, second_top_left.y);
12331 builder.line_to(first_bottom_left - curve_width);
12332 if self.corner_radius > Pixels::ZERO {
12333 builder.curve_to(first_bottom_left - curve_height, first_bottom_left);
12334 }
12335 }
12336
12337 builder.line_to(first_top_left + curve_height);
12338 if self.corner_radius > Pixels::ZERO {
12339 builder.curve_to(first_top_left + top_curve_width, first_top_left);
12340 }
12341 builder.line_to(first_top_right - top_curve_width);
12342
12343 if let Ok(path) = builder.build() {
12344 window.paint_path(path, self.color);
12345 }
12346 }
12347}
12348
12349pub(crate) struct StickyHeader {
12350 pub sticky_row: DisplayRow,
12351 pub start_point: Point,
12352 pub offset: ScrollOffset,
12353}
12354
12355enum CursorPopoverType {
12356 CodeContextMenu,
12357 EditPrediction,
12358}
12359
12360pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
12361 (delta.pow(1.2) / 100.0).min(px(3.0)).into()
12362}
12363
12364fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
12365 (delta.pow(1.2) / 300.0).into()
12366}
12367
12368pub fn register_action<T: Action>(
12369 editor: &Entity<Editor>,
12370 window: &mut Window,
12371 listener: impl Fn(&mut Editor, &T, &mut Window, &mut Context<Editor>) + 'static,
12372) {
12373 let editor = editor.clone();
12374 window.on_action(TypeId::of::<T>(), move |action, phase, window, cx| {
12375 let action = action.downcast_ref().unwrap();
12376 if phase == DispatchPhase::Bubble {
12377 editor.update(cx, |editor, cx| {
12378 listener(editor, action, window, cx);
12379 })
12380 }
12381 })
12382}
12383
12384/// Shared between `prepaint` and `compute_auto_height_layout` to ensure
12385/// both full and auto-height editors compute wrap widths consistently.
12386fn calculate_wrap_width(
12387 soft_wrap: SoftWrap,
12388 editor_width: Pixels,
12389 em_width: Pixels,
12390) -> Option<Pixels> {
12391 let wrap_width_for = |column: u32| (column as f32 * em_width).ceil();
12392
12393 match soft_wrap {
12394 SoftWrap::GitDiff => None,
12395 SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
12396 SoftWrap::EditorWidth => Some(editor_width),
12397 SoftWrap::Column(column) => Some(wrap_width_for(column)),
12398 SoftWrap::Bounded(column) => Some(editor_width.min(wrap_width_for(column))),
12399 }
12400}
12401
12402fn compute_auto_height_layout(
12403 editor: &mut Editor,
12404 min_lines: usize,
12405 max_lines: Option<usize>,
12406 known_dimensions: Size<Option<Pixels>>,
12407 available_width: AvailableSpace,
12408 window: &mut Window,
12409 cx: &mut Context<Editor>,
12410) -> Option<Size<Pixels>> {
12411 let width = known_dimensions.width.or({
12412 if let AvailableSpace::Definite(available_width) = available_width {
12413 Some(available_width)
12414 } else {
12415 None
12416 }
12417 })?;
12418 if let Some(height) = known_dimensions.height {
12419 return Some(size(width, height));
12420 }
12421
12422 let style = editor.style.as_ref().unwrap();
12423 let font_id = window.text_system().resolve_font(&style.text.font());
12424 let font_size = style.text.font_size.to_pixels(window.rem_size());
12425 let line_height = style.text.line_height_in_pixels(window.rem_size());
12426 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
12427
12428 let mut snapshot = editor.snapshot(window, cx);
12429 let gutter_dimensions = snapshot.gutter_dimensions(font_id, font_size, style, window, cx);
12430
12431 editor.gutter_dimensions = gutter_dimensions;
12432 let text_width = width - gutter_dimensions.width;
12433 let overscroll = size(em_width, px(0.));
12434
12435 let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
12436 let wrap_width = calculate_wrap_width(editor.soft_wrap_mode(cx), editor_width, em_width);
12437 if wrap_width.is_some() && editor.set_wrap_width(wrap_width, cx) {
12438 snapshot = editor.snapshot(window, cx);
12439 }
12440
12441 let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
12442
12443 let min_height = line_height * min_lines as f32;
12444 let content_height = scroll_height.max(min_height);
12445
12446 let final_height = if let Some(max_lines) = max_lines {
12447 let max_height = line_height * max_lines as f32;
12448 content_height.min(max_height)
12449 } else {
12450 content_height
12451 };
12452
12453 Some(size(width, final_height))
12454}
12455
12456#[cfg(test)]
12457mod tests {
12458 use super::*;
12459 use crate::{
12460 Editor, MultiBuffer, SelectionEffects,
12461 display_map::{BlockPlacement, BlockProperties},
12462 editor_tests::{init_test, update_test_language_settings},
12463 };
12464 use gpui::{TestAppContext, VisualTestContext};
12465 use language::{Buffer, language_settings, tree_sitter_python};
12466 use log::info;
12467 use rand::{RngCore, rngs::StdRng};
12468 use std::num::NonZeroU32;
12469 use util::test::sample_text;
12470
12471 #[gpui::test]
12472 async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) {
12473 init_test(cx, |_| {});
12474 let window = cx.add_window(|window, cx| {
12475 let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
12476 let mut editor = Editor::new(
12477 EditorMode::AutoHeight {
12478 min_lines: 1,
12479 max_lines: None,
12480 },
12481 buffer,
12482 None,
12483 window,
12484 cx,
12485 );
12486 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
12487 editor
12488 });
12489 let cx = &mut VisualTestContext::from_window(*window, cx);
12490 let editor = window.root(cx).unwrap();
12491 let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
12492
12493 for x in 1..=100 {
12494 let (_, state) = cx.draw(
12495 Default::default(),
12496 size(px(200. + 0.13 * x as f32), px(500.)),
12497 |_, _| EditorElement::new(&editor, style.clone()),
12498 );
12499
12500 assert!(
12501 state.position_map.scroll_max.x == 0.,
12502 "Soft wrapped editor should have no horizontal scrolling!"
12503 );
12504 }
12505 }
12506
12507 #[gpui::test]
12508 async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) {
12509 init_test(cx, |_| {});
12510 let window = cx.add_window(|window, cx| {
12511 let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
12512 let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
12513 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
12514 editor
12515 });
12516 let cx = &mut VisualTestContext::from_window(*window, cx);
12517 let editor = window.root(cx).unwrap();
12518 let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
12519
12520 for x in 1..=100 {
12521 let (_, state) = cx.draw(
12522 Default::default(),
12523 size(px(200. + 0.13 * x as f32), px(500.)),
12524 |_, _| EditorElement::new(&editor, style.clone()),
12525 );
12526
12527 assert!(
12528 state.position_map.scroll_max.x == 0.,
12529 "Soft wrapped editor should have no horizontal scrolling!"
12530 );
12531 }
12532 }
12533
12534 #[gpui::test]
12535 fn test_layout_line_numbers(cx: &mut TestAppContext) {
12536 init_test(cx, |_| {});
12537 let window = cx.add_window(|window, cx| {
12538 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
12539 Editor::new(EditorMode::full(), buffer, None, window, cx)
12540 });
12541
12542 let editor = window.root(cx).unwrap();
12543 let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
12544 let line_height = window
12545 .update(cx, |_, window, _| {
12546 style.text.line_height_in_pixels(window.rem_size())
12547 })
12548 .unwrap();
12549 let element = EditorElement::new(&editor, style);
12550 let snapshot = window
12551 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
12552 .unwrap();
12553
12554 let layouts = cx
12555 .update_window(*window, |_, window, cx| {
12556 element.layout_line_numbers(
12557 None,
12558 GutterDimensions {
12559 left_padding: Pixels::ZERO,
12560 right_padding: Pixels::ZERO,
12561 width: px(30.0),
12562 margin: Pixels::ZERO,
12563 git_blame_entries_width: None,
12564 },
12565 line_height,
12566 gpui::Point::default(),
12567 DisplayRow(0)..DisplayRow(6),
12568 &(0..6)
12569 .map(|row| RowInfo {
12570 buffer_row: Some(row),
12571 ..Default::default()
12572 })
12573 .collect::<Vec<_>>(),
12574 &BTreeMap::default(),
12575 Some(DisplayRow(0)),
12576 &snapshot,
12577 window,
12578 cx,
12579 )
12580 })
12581 .unwrap();
12582 assert_eq!(layouts.len(), 6);
12583
12584 let relative_rows = window
12585 .update(cx, |editor, window, cx| {
12586 let snapshot = editor.snapshot(window, cx);
12587 snapshot.calculate_relative_line_numbers(
12588 &(DisplayRow(0)..DisplayRow(6)),
12589 DisplayRow(3),
12590 false,
12591 )
12592 })
12593 .unwrap();
12594 assert_eq!(relative_rows[&DisplayRow(0)], 3);
12595 assert_eq!(relative_rows[&DisplayRow(1)], 2);
12596 assert_eq!(relative_rows[&DisplayRow(2)], 1);
12597 // current line has no relative number
12598 assert!(!relative_rows.contains_key(&DisplayRow(3)));
12599 assert_eq!(relative_rows[&DisplayRow(4)], 1);
12600 assert_eq!(relative_rows[&DisplayRow(5)], 2);
12601
12602 // works if cursor is before screen
12603 let relative_rows = window
12604 .update(cx, |editor, window, cx| {
12605 let snapshot = editor.snapshot(window, cx);
12606 snapshot.calculate_relative_line_numbers(
12607 &(DisplayRow(3)..DisplayRow(6)),
12608 DisplayRow(1),
12609 false,
12610 )
12611 })
12612 .unwrap();
12613 assert_eq!(relative_rows.len(), 3);
12614 assert_eq!(relative_rows[&DisplayRow(3)], 2);
12615 assert_eq!(relative_rows[&DisplayRow(4)], 3);
12616 assert_eq!(relative_rows[&DisplayRow(5)], 4);
12617
12618 // works if cursor is after screen
12619 let relative_rows = window
12620 .update(cx, |editor, window, cx| {
12621 let snapshot = editor.snapshot(window, cx);
12622 snapshot.calculate_relative_line_numbers(
12623 &(DisplayRow(0)..DisplayRow(3)),
12624 DisplayRow(6),
12625 false,
12626 )
12627 })
12628 .unwrap();
12629 assert_eq!(relative_rows.len(), 3);
12630 assert_eq!(relative_rows[&DisplayRow(0)], 5);
12631 assert_eq!(relative_rows[&DisplayRow(1)], 4);
12632 assert_eq!(relative_rows[&DisplayRow(2)], 3);
12633
12634 const DELETED_LINE: u32 = 3;
12635 let layouts = cx
12636 .update_window(*window, |_, window, cx| {
12637 element.layout_line_numbers(
12638 None,
12639 GutterDimensions {
12640 left_padding: Pixels::ZERO,
12641 right_padding: Pixels::ZERO,
12642 width: px(30.0),
12643 margin: Pixels::ZERO,
12644 git_blame_entries_width: None,
12645 },
12646 line_height,
12647 gpui::Point::default(),
12648 DisplayRow(0)..DisplayRow(6),
12649 &(0..6)
12650 .map(|row| RowInfo {
12651 buffer_row: Some(row),
12652 diff_status: (row == DELETED_LINE).then(|| {
12653 DiffHunkStatus::deleted(
12654 buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk,
12655 )
12656 }),
12657 ..Default::default()
12658 })
12659 .collect::<Vec<_>>(),
12660 &BTreeMap::default(),
12661 Some(DisplayRow(0)),
12662 &snapshot,
12663 window,
12664 cx,
12665 )
12666 })
12667 .unwrap();
12668 assert_eq!(layouts.len(), 5,);
12669 assert!(
12670 layouts.get(&MultiBufferRow(DELETED_LINE)).is_none(),
12671 "Deleted line should not have a line number"
12672 );
12673 }
12674
12675 #[gpui::test]
12676 async fn test_layout_line_numbers_with_folded_lines(cx: &mut TestAppContext) {
12677 init_test(cx, |_| {});
12678
12679 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
12680
12681 let window = cx.add_window(|window, cx| {
12682 let buffer = cx.new(|cx| {
12683 Buffer::local(
12684 indoc::indoc! {"
12685 fn test() -> int {
12686 return 2;
12687 }
12688
12689 fn another_test() -> int {
12690 # This is a very peculiar method that is hard to grasp.
12691 return 4;
12692 }
12693 "},
12694 cx,
12695 )
12696 .with_language(python_lang, cx)
12697 });
12698
12699 let buffer = MultiBuffer::build_from_buffer(buffer, cx);
12700 Editor::new(EditorMode::full(), buffer, None, window, cx)
12701 });
12702
12703 let editor = window.root(cx).unwrap();
12704 let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
12705 let line_height = window
12706 .update(cx, |_, window, _| {
12707 style.text.line_height_in_pixels(window.rem_size())
12708 })
12709 .unwrap();
12710 let element = EditorElement::new(&editor, style);
12711 let snapshot = window
12712 .update(cx, |editor, window, cx| {
12713 editor.fold_at(MultiBufferRow(0), window, cx);
12714 editor.snapshot(window, cx)
12715 })
12716 .unwrap();
12717
12718 let layouts = cx
12719 .update_window(*window, |_, window, cx| {
12720 element.layout_line_numbers(
12721 None,
12722 GutterDimensions {
12723 left_padding: Pixels::ZERO,
12724 right_padding: Pixels::ZERO,
12725 width: px(30.0),
12726 margin: Pixels::ZERO,
12727 git_blame_entries_width: None,
12728 },
12729 line_height,
12730 gpui::Point::default(),
12731 DisplayRow(0)..DisplayRow(6),
12732 &(0..6)
12733 .map(|row| RowInfo {
12734 buffer_row: Some(row),
12735 ..Default::default()
12736 })
12737 .collect::<Vec<_>>(),
12738 &BTreeMap::default(),
12739 Some(DisplayRow(3)),
12740 &snapshot,
12741 window,
12742 cx,
12743 )
12744 })
12745 .unwrap();
12746 assert_eq!(layouts.len(), 6);
12747
12748 let relative_rows = window
12749 .update(cx, |editor, window, cx| {
12750 let snapshot = editor.snapshot(window, cx);
12751 snapshot.calculate_relative_line_numbers(
12752 &(DisplayRow(0)..DisplayRow(6)),
12753 DisplayRow(3),
12754 false,
12755 )
12756 })
12757 .unwrap();
12758 assert_eq!(relative_rows[&DisplayRow(0)], 3);
12759 assert_eq!(relative_rows[&DisplayRow(1)], 2);
12760 assert_eq!(relative_rows[&DisplayRow(2)], 1);
12761 // current line has no relative number
12762 assert!(!relative_rows.contains_key(&DisplayRow(3)));
12763 assert_eq!(relative_rows[&DisplayRow(4)], 1);
12764 assert_eq!(relative_rows[&DisplayRow(5)], 2);
12765 }
12766
12767 #[gpui::test]
12768 fn test_layout_line_numbers_wrapping(cx: &mut TestAppContext) {
12769 init_test(cx, |_| {});
12770 let window = cx.add_window(|window, cx| {
12771 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
12772 Editor::new(EditorMode::full(), buffer, None, window, cx)
12773 });
12774
12775 update_test_language_settings(cx, &|s| {
12776 s.defaults.preferred_line_length = Some(5_u32);
12777 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
12778 });
12779
12780 let editor = window.root(cx).unwrap();
12781 let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
12782 let line_height = window
12783 .update(cx, |_, window, _| {
12784 style.text.line_height_in_pixels(window.rem_size())
12785 })
12786 .unwrap();
12787 let element = EditorElement::new(&editor, style);
12788 let snapshot = window
12789 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
12790 .unwrap();
12791
12792 let layouts = cx
12793 .update_window(*window, |_, window, cx| {
12794 element.layout_line_numbers(
12795 None,
12796 GutterDimensions {
12797 left_padding: Pixels::ZERO,
12798 right_padding: Pixels::ZERO,
12799 width: px(30.0),
12800 margin: Pixels::ZERO,
12801 git_blame_entries_width: None,
12802 },
12803 line_height,
12804 gpui::Point::default(),
12805 DisplayRow(0)..DisplayRow(6),
12806 &(0..6)
12807 .map(|row| RowInfo {
12808 buffer_row: Some(row),
12809 ..Default::default()
12810 })
12811 .collect::<Vec<_>>(),
12812 &BTreeMap::default(),
12813 Some(DisplayRow(0)),
12814 &snapshot,
12815 window,
12816 cx,
12817 )
12818 })
12819 .unwrap();
12820 assert_eq!(layouts.len(), 3);
12821
12822 let relative_rows = window
12823 .update(cx, |editor, window, cx| {
12824 let snapshot = editor.snapshot(window, cx);
12825 snapshot.calculate_relative_line_numbers(
12826 &(DisplayRow(0)..DisplayRow(6)),
12827 DisplayRow(3),
12828 true,
12829 )
12830 })
12831 .unwrap();
12832
12833 assert_eq!(relative_rows[&DisplayRow(0)], 3);
12834 assert_eq!(relative_rows[&DisplayRow(1)], 2);
12835 assert_eq!(relative_rows[&DisplayRow(2)], 1);
12836 // current line has no relative number
12837 assert!(!relative_rows.contains_key(&DisplayRow(3)));
12838 assert_eq!(relative_rows[&DisplayRow(4)], 1);
12839 assert_eq!(relative_rows[&DisplayRow(5)], 2);
12840
12841 let layouts = cx
12842 .update_window(*window, |_, window, cx| {
12843 element.layout_line_numbers(
12844 None,
12845 GutterDimensions {
12846 left_padding: Pixels::ZERO,
12847 right_padding: Pixels::ZERO,
12848 width: px(30.0),
12849 margin: Pixels::ZERO,
12850 git_blame_entries_width: None,
12851 },
12852 line_height,
12853 gpui::Point::default(),
12854 DisplayRow(0)..DisplayRow(6),
12855 &(0..6)
12856 .map(|row| RowInfo {
12857 buffer_row: Some(row),
12858 diff_status: Some(DiffHunkStatus::deleted(
12859 buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk,
12860 )),
12861 ..Default::default()
12862 })
12863 .collect::<Vec<_>>(),
12864 &BTreeMap::from_iter([(DisplayRow(0), LineHighlightSpec::default())]),
12865 Some(DisplayRow(0)),
12866 &snapshot,
12867 window,
12868 cx,
12869 )
12870 })
12871 .unwrap();
12872 assert!(
12873 layouts.is_empty(),
12874 "Deleted lines should have no line number"
12875 );
12876
12877 let relative_rows = window
12878 .update(cx, |editor, window, cx| {
12879 let snapshot = editor.snapshot(window, cx);
12880 snapshot.calculate_relative_line_numbers(
12881 &(DisplayRow(0)..DisplayRow(6)),
12882 DisplayRow(3),
12883 true,
12884 )
12885 })
12886 .unwrap();
12887
12888 // Deleted lines should still have relative numbers
12889 assert_eq!(relative_rows[&DisplayRow(0)], 3);
12890 assert_eq!(relative_rows[&DisplayRow(1)], 2);
12891 assert_eq!(relative_rows[&DisplayRow(2)], 1);
12892 // current line, even if deleted, has no relative number
12893 assert!(!relative_rows.contains_key(&DisplayRow(3)));
12894 assert_eq!(relative_rows[&DisplayRow(4)], 1);
12895 assert_eq!(relative_rows[&DisplayRow(5)], 2);
12896 }
12897
12898 #[gpui::test]
12899 async fn test_vim_visual_selections(cx: &mut TestAppContext) {
12900 init_test(cx, |_| {});
12901
12902 let window = cx.add_window(|window, cx| {
12903 let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
12904 Editor::new(EditorMode::full(), buffer, None, window, cx)
12905 });
12906 let cx = &mut VisualTestContext::from_window(*window, cx);
12907 let editor = window.root(cx).unwrap();
12908 let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
12909
12910 window
12911 .update(cx, |editor, window, cx| {
12912 editor.cursor_offset_on_selection = true;
12913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12914 s.select_ranges([
12915 Point::new(0, 0)..Point::new(1, 0),
12916 Point::new(3, 2)..Point::new(3, 3),
12917 Point::new(5, 6)..Point::new(6, 0),
12918 ]);
12919 });
12920 })
12921 .unwrap();
12922
12923 let (_, state) = cx.draw(
12924 point(px(500.), px(500.)),
12925 size(px(500.), px(500.)),
12926 |_, _| EditorElement::new(&editor, style),
12927 );
12928
12929 assert_eq!(state.selections.len(), 1);
12930 let local_selections = &state.selections[0].1;
12931 assert_eq!(local_selections.len(), 3);
12932 // moves cursor back one line
12933 assert_eq!(
12934 local_selections[0].head,
12935 DisplayPoint::new(DisplayRow(0), 6)
12936 );
12937 assert_eq!(
12938 local_selections[0].range,
12939 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0)
12940 );
12941
12942 // moves cursor back one column
12943 assert_eq!(
12944 local_selections[1].range,
12945 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 3)
12946 );
12947 assert_eq!(
12948 local_selections[1].head,
12949 DisplayPoint::new(DisplayRow(3), 2)
12950 );
12951
12952 // leaves cursor on the max point
12953 assert_eq!(
12954 local_selections[2].range,
12955 DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(6), 0)
12956 );
12957 assert_eq!(
12958 local_selections[2].head,
12959 DisplayPoint::new(DisplayRow(6), 0)
12960 );
12961
12962 // active lines does not include 1 (even though the range of the selection does)
12963 assert_eq!(
12964 state.active_rows.keys().cloned().collect::<Vec<_>>(),
12965 vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
12966 );
12967 }
12968
12969 #[gpui::test]
12970 fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
12971 init_test(cx, |_| {});
12972
12973 let window = cx.add_window(|window, cx| {
12974 let buffer = MultiBuffer::build_simple("", cx);
12975 Editor::new(EditorMode::full(), buffer, None, window, cx)
12976 });
12977 let cx = &mut VisualTestContext::from_window(*window, cx);
12978 let editor = window.root(cx).unwrap();
12979 let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
12980 window
12981 .update(cx, |editor, window, cx| {
12982 editor.set_placeholder_text("hello", window, cx);
12983 editor.insert_blocks(
12984 [BlockProperties {
12985 style: BlockStyle::Fixed,
12986 placement: BlockPlacement::Above(Anchor::Min),
12987 height: Some(3),
12988 render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
12989 priority: 0,
12990 }],
12991 None,
12992 cx,
12993 );
12994
12995 // Blur the editor so that it displays placeholder text.
12996 window.blur();
12997 })
12998 .unwrap();
12999
13000 let (_, state) = cx.draw(
13001 point(px(500.), px(500.)),
13002 size(px(500.), px(500.)),
13003 |_, _| EditorElement::new(&editor, style),
13004 );
13005 assert_eq!(state.position_map.line_layouts.len(), 4);
13006 assert_eq!(state.line_numbers.len(), 1);
13007 assert_eq!(
13008 state
13009 .line_numbers
13010 .get(&MultiBufferRow(0))
13011 .map(|line_number| line_number
13012 .segments
13013 .first()
13014 .unwrap()
13015 .shaped_line
13016 .text
13017 .as_ref()),
13018 Some("1")
13019 );
13020 }
13021
13022 #[gpui::test]
13023 fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
13024 const TAB_SIZE: u32 = 4;
13025
13026 let input_text = "\t \t|\t| a b";
13027 let expected_invisibles = vec![
13028 Invisible::Tab {
13029 line_start_offset: 0,
13030 line_end_offset: TAB_SIZE as usize,
13031 },
13032 Invisible::Whitespace {
13033 line_offset: TAB_SIZE as usize,
13034 },
13035 Invisible::Tab {
13036 line_start_offset: TAB_SIZE as usize + 1,
13037 line_end_offset: TAB_SIZE as usize * 2,
13038 },
13039 Invisible::Tab {
13040 line_start_offset: TAB_SIZE as usize * 2 + 1,
13041 line_end_offset: TAB_SIZE as usize * 3,
13042 },
13043 Invisible::Whitespace {
13044 line_offset: TAB_SIZE as usize * 3 + 1,
13045 },
13046 Invisible::Whitespace {
13047 line_offset: TAB_SIZE as usize * 3 + 3,
13048 },
13049 ];
13050 assert_eq!(
13051 expected_invisibles.len(),
13052 input_text
13053 .chars()
13054 .filter(|initial_char| initial_char.is_whitespace())
13055 .count(),
13056 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
13057 );
13058
13059 for show_line_numbers in [true, false] {
13060 init_test(cx, |s| {
13061 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
13062 s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
13063 });
13064
13065 let actual_invisibles = collect_invisibles_from_new_editor(
13066 cx,
13067 EditorMode::full(),
13068 input_text,
13069 px(500.0),
13070 show_line_numbers,
13071 );
13072
13073 assert_eq!(expected_invisibles, actual_invisibles);
13074 }
13075 }
13076
13077 #[gpui::test]
13078 fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
13079 init_test(cx, |s| {
13080 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
13081 s.defaults.tab_size = NonZeroU32::new(4);
13082 });
13083
13084 for editor_mode_without_invisibles in [
13085 EditorMode::SingleLine,
13086 EditorMode::AutoHeight {
13087 min_lines: 1,
13088 max_lines: Some(100),
13089 },
13090 ] {
13091 for show_line_numbers in [true, false] {
13092 let invisibles = collect_invisibles_from_new_editor(
13093 cx,
13094 editor_mode_without_invisibles.clone(),
13095 "\t\t\t| | a b",
13096 px(500.0),
13097 show_line_numbers,
13098 );
13099 assert!(
13100 invisibles.is_empty(),
13101 "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"
13102 );
13103 }
13104 }
13105 }
13106
13107 #[gpui::test]
13108 fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
13109 let tab_size = 4;
13110 let input_text = "a\tbcd ".repeat(9);
13111 let repeated_invisibles = [
13112 Invisible::Tab {
13113 line_start_offset: 1,
13114 line_end_offset: tab_size as usize,
13115 },
13116 Invisible::Whitespace {
13117 line_offset: tab_size as usize + 3,
13118 },
13119 Invisible::Whitespace {
13120 line_offset: tab_size as usize + 4,
13121 },
13122 Invisible::Whitespace {
13123 line_offset: tab_size as usize + 5,
13124 },
13125 Invisible::Whitespace {
13126 line_offset: tab_size as usize + 6,
13127 },
13128 Invisible::Whitespace {
13129 line_offset: tab_size as usize + 7,
13130 },
13131 ];
13132 let expected_invisibles = std::iter::once(repeated_invisibles)
13133 .cycle()
13134 .take(9)
13135 .flatten()
13136 .collect::<Vec<_>>();
13137 assert_eq!(
13138 expected_invisibles.len(),
13139 input_text
13140 .chars()
13141 .filter(|initial_char| initial_char.is_whitespace())
13142 .count(),
13143 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
13144 );
13145 info!("Expected invisibles: {expected_invisibles:?}");
13146
13147 init_test(cx, |_| {});
13148
13149 // Put the same string with repeating whitespace pattern into editors of various size,
13150 // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
13151 let resize_step = 10.0;
13152 let mut editor_width = 200.0;
13153 while editor_width <= 1000.0 {
13154 for show_line_numbers in [true, false] {
13155 update_test_language_settings(cx, &|s| {
13156 s.defaults.tab_size = NonZeroU32::new(tab_size);
13157 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
13158 s.defaults.preferred_line_length = Some(editor_width as u32);
13159 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
13160 });
13161
13162 let actual_invisibles = collect_invisibles_from_new_editor(
13163 cx,
13164 EditorMode::full(),
13165 &input_text,
13166 px(editor_width),
13167 show_line_numbers,
13168 );
13169
13170 // Whatever the editor size is, ensure it has the same invisible kinds in the same order
13171 // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
13172 let mut i = 0;
13173 for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
13174 i = actual_index;
13175 match expected_invisibles.get(i) {
13176 Some(expected_invisible) => match (expected_invisible, actual_invisible) {
13177 (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
13178 | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
13179 _ => {
13180 panic!(
13181 "At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}"
13182 )
13183 }
13184 },
13185 None => {
13186 panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
13187 }
13188 }
13189 }
13190 let missing_expected_invisibles = &expected_invisibles[i + 1..];
13191 assert!(
13192 missing_expected_invisibles.is_empty(),
13193 "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
13194 );
13195
13196 editor_width += resize_step;
13197 }
13198 }
13199 }
13200
13201 fn collect_invisibles_from_new_editor(
13202 cx: &mut TestAppContext,
13203 editor_mode: EditorMode,
13204 input_text: &str,
13205 editor_width: Pixels,
13206 show_line_numbers: bool,
13207 ) -> Vec<Invisible> {
13208 info!(
13209 "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
13210 f32::from(editor_width)
13211 );
13212 let window = cx.add_window(|window, cx| {
13213 let buffer = MultiBuffer::build_simple(input_text, cx);
13214 Editor::new(editor_mode, buffer, None, window, cx)
13215 });
13216 let cx = &mut VisualTestContext::from_window(*window, cx);
13217 let editor = window.root(cx).unwrap();
13218
13219 let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
13220 window
13221 .update(cx, |editor, _, cx| {
13222 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
13223 editor.set_wrap_width(Some(editor_width), cx);
13224 editor.set_show_line_numbers(show_line_numbers, cx);
13225 })
13226 .unwrap();
13227 let (_, state) = cx.draw(
13228 point(px(500.), px(500.)),
13229 size(px(500.), px(500.)),
13230 |_, _| EditorElement::new(&editor, style),
13231 );
13232 state
13233 .position_map
13234 .line_layouts
13235 .iter()
13236 .flat_map(|line_with_invisibles| &line_with_invisibles.invisibles)
13237 .cloned()
13238 .collect()
13239 }
13240
13241 #[gpui::test]
13242 fn test_merge_overlapping_ranges() {
13243 let base_bg = Hsla::white();
13244 let color1 = Hsla {
13245 h: 0.0,
13246 s: 0.5,
13247 l: 0.5,
13248 a: 0.5,
13249 };
13250 let color2 = Hsla {
13251 h: 120.0,
13252 s: 0.5,
13253 l: 0.5,
13254 a: 0.5,
13255 };
13256
13257 let display_point = |col| DisplayPoint::new(DisplayRow(0), col);
13258 let cols = |v: &Vec<(Range<DisplayPoint>, Hsla)>| -> Vec<(u32, u32)> {
13259 v.iter()
13260 .map(|(r, _)| (r.start.column(), r.end.column()))
13261 .collect()
13262 };
13263
13264 // Test overlapping ranges blend colors
13265 let overlapping = vec![
13266 (display_point(5)..display_point(15), color1),
13267 (display_point(10)..display_point(20), color2),
13268 ];
13269 let result = EditorElement::merge_overlapping_ranges(overlapping, base_bg);
13270 assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
13271
13272 // Test middle segment should have blended color
13273 let blended = Hsla::blend(Hsla::blend(base_bg, color1), color2);
13274 assert_eq!(result[1].1, blended);
13275
13276 // Test adjacent same-color ranges merge
13277 let adjacent_same = vec![
13278 (display_point(5)..display_point(10), color1),
13279 (display_point(10)..display_point(15), color1),
13280 ];
13281 let result = EditorElement::merge_overlapping_ranges(adjacent_same, base_bg);
13282 assert_eq!(cols(&result), vec![(5, 15)]);
13283
13284 // Test contained range splits
13285 let contained = vec![
13286 (display_point(5)..display_point(20), color1),
13287 (display_point(10)..display_point(15), color2),
13288 ];
13289 let result = EditorElement::merge_overlapping_ranges(contained, base_bg);
13290 assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
13291
13292 // Test multiple overlaps split at every boundary
13293 let color3 = Hsla {
13294 h: 240.0,
13295 s: 0.5,
13296 l: 0.5,
13297 a: 0.5,
13298 };
13299 let complex = vec![
13300 (display_point(5)..display_point(12), color1),
13301 (display_point(8)..display_point(16), color2),
13302 (display_point(10)..display_point(14), color3),
13303 ];
13304 let result = EditorElement::merge_overlapping_ranges(complex, base_bg);
13305 assert_eq!(
13306 cols(&result),
13307 vec![(5, 8), (8, 10), (10, 12), (12, 14), (14, 16)]
13308 );
13309 }
13310
13311 #[gpui::test]
13312 fn test_bg_segments_per_row() {
13313 let base_bg = Hsla::white();
13314
13315 // Case A: selection spans three display rows: row 1 [5, end), full row 2, row 3 [0, 7)
13316 {
13317 let selection_color = Hsla {
13318 h: 200.0,
13319 s: 0.5,
13320 l: 0.5,
13321 a: 0.5,
13322 };
13323 let player_color = PlayerColor {
13324 cursor: selection_color,
13325 background: selection_color,
13326 selection: selection_color,
13327 };
13328
13329 let spanning_selection = SelectionLayout {
13330 head: DisplayPoint::new(DisplayRow(3), 7),
13331 cursor_shape: CursorShape::Bar,
13332 is_newest: true,
13333 is_local: true,
13334 range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 7),
13335 active_rows: DisplayRow(1)..DisplayRow(4),
13336 user_name: None,
13337 };
13338
13339 let selections = vec![(player_color, vec![spanning_selection])];
13340 let result = EditorElement::bg_segments_per_row(
13341 DisplayRow(0)..DisplayRow(5),
13342 &selections,
13343 &[],
13344 base_bg,
13345 );
13346
13347 assert_eq!(result.len(), 5);
13348 assert!(result[0].is_empty());
13349 assert_eq!(result[1].len(), 1);
13350 assert_eq!(result[2].len(), 1);
13351 assert_eq!(result[3].len(), 1);
13352 assert!(result[4].is_empty());
13353
13354 assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
13355 assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
13356 assert_eq!(result[1][0].0.end.column(), u32::MAX);
13357 assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
13358 assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
13359 assert_eq!(result[2][0].0.end.column(), u32::MAX);
13360 assert_eq!(result[3][0].0.start, DisplayPoint::new(DisplayRow(3), 0));
13361 assert_eq!(result[3][0].0.end, DisplayPoint::new(DisplayRow(3), 7));
13362 }
13363
13364 // Case B: selection ends exactly at the start of row 3, excluding row 3
13365 {
13366 let selection_color = Hsla {
13367 h: 120.0,
13368 s: 0.5,
13369 l: 0.5,
13370 a: 0.5,
13371 };
13372 let player_color = PlayerColor {
13373 cursor: selection_color,
13374 background: selection_color,
13375 selection: selection_color,
13376 };
13377
13378 let selection = SelectionLayout {
13379 head: DisplayPoint::new(DisplayRow(2), 0),
13380 cursor_shape: CursorShape::Bar,
13381 is_newest: true,
13382 is_local: true,
13383 range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 0),
13384 active_rows: DisplayRow(1)..DisplayRow(3),
13385 user_name: None,
13386 };
13387
13388 let selections = vec![(player_color, vec![selection])];
13389 let result = EditorElement::bg_segments_per_row(
13390 DisplayRow(0)..DisplayRow(4),
13391 &selections,
13392 &[],
13393 base_bg,
13394 );
13395
13396 assert_eq!(result.len(), 4);
13397 assert!(result[0].is_empty());
13398 assert_eq!(result[1].len(), 1);
13399 assert_eq!(result[2].len(), 1);
13400 assert!(result[3].is_empty());
13401
13402 assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
13403 assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
13404 assert_eq!(result[1][0].0.end.column(), u32::MAX);
13405 assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
13406 assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
13407 assert_eq!(result[2][0].0.end.column(), u32::MAX);
13408 }
13409 }
13410
13411 #[cfg(test)]
13412 fn generate_test_run(len: usize, color: Hsla) -> TextRun {
13413 TextRun {
13414 len,
13415 color,
13416 ..Default::default()
13417 }
13418 }
13419
13420 #[gpui::test]
13421 fn test_split_runs_by_bg_segments(cx: &mut gpui::TestAppContext) {
13422 init_test(cx, |_| {});
13423
13424 let dx = |start: u32, end: u32| {
13425 DisplayPoint::new(DisplayRow(0), start)..DisplayPoint::new(DisplayRow(0), end)
13426 };
13427
13428 let text_color = Hsla {
13429 h: 210.0,
13430 s: 0.1,
13431 l: 0.4,
13432 a: 1.0,
13433 };
13434 let bg_1 = Hsla {
13435 h: 30.0,
13436 s: 0.6,
13437 l: 0.8,
13438 a: 1.0,
13439 };
13440 let bg_2 = Hsla {
13441 h: 200.0,
13442 s: 0.6,
13443 l: 0.2,
13444 a: 1.0,
13445 };
13446 let min_contrast = 45.0;
13447 let adjusted_bg1 = ensure_minimum_contrast(text_color, bg_1, min_contrast);
13448 let adjusted_bg2 = ensure_minimum_contrast(text_color, bg_2, min_contrast);
13449
13450 // Case A: single run; disjoint segments inside the run
13451 {
13452 let runs = vec![generate_test_run(20, text_color)];
13453 let segs = vec![(dx(5, 10), bg_1), (dx(12, 16), bg_2)];
13454 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
13455 // Expected slices: [0,5) [5,10) [10,12) [12,16) [16,20)
13456 assert_eq!(
13457 out.iter().map(|r| r.len).collect::<Vec<_>>(),
13458 vec![5, 5, 2, 4, 4]
13459 );
13460 assert_eq!(out[0].color, text_color);
13461 assert_eq!(out[1].color, adjusted_bg1);
13462 assert_eq!(out[2].color, text_color);
13463 assert_eq!(out[3].color, adjusted_bg2);
13464 assert_eq!(out[4].color, text_color);
13465 }
13466
13467 // Case B: multiple runs; segment extends to end of line (u32::MAX)
13468 {
13469 let runs = vec![
13470 generate_test_run(8, text_color),
13471 generate_test_run(7, text_color),
13472 ];
13473 let segs = vec![(dx(6, u32::MAX), bg_1)];
13474 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
13475 // Expected slices across runs: [0,6) [6,8) | [0,7)
13476 assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![6, 2, 7]);
13477 assert_eq!(out[0].color, text_color);
13478 assert_eq!(out[1].color, adjusted_bg1);
13479 assert_eq!(out[2].color, adjusted_bg1);
13480 }
13481
13482 // Case C: multi-byte characters
13483 {
13484 // for text: "Hello π δΈη!"
13485 let runs = vec![
13486 generate_test_run(5, text_color), // "Hello"
13487 generate_test_run(6, text_color), // " π "
13488 generate_test_run(6, text_color), // "δΈη"
13489 generate_test_run(1, text_color), // "!"
13490 ];
13491 // selecting "π δΈ"
13492 let segs = vec![(dx(6, 14), bg_1)];
13493 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
13494 // "Hello" | " " | "π " | "δΈ" | "η" | "!"
13495 assert_eq!(
13496 out.iter().map(|r| r.len).collect::<Vec<_>>(),
13497 vec![5, 1, 5, 3, 3, 1]
13498 );
13499 assert_eq!(out[0].color, text_color); // "Hello"
13500 assert_eq!(out[2].color, adjusted_bg1); // "π "
13501 assert_eq!(out[3].color, adjusted_bg1); // "δΈ"
13502 assert_eq!(out[4].color, text_color); // "η"
13503 assert_eq!(out[5].color, text_color); // "!"
13504 }
13505
13506 // Case D: split multiple consecutive text runs with segments
13507 {
13508 let segs = vec![
13509 (dx(2, 4), bg_1), // selecting "cd"
13510 (dx(4, 8), bg_2), // selecting "efgh"
13511 (dx(9, 11), bg_1), // selecting "jk"
13512 (dx(12, 16), bg_2), // selecting "mnop"
13513 (dx(18, 19), bg_1), // selecting "s"
13514 ];
13515
13516 // for text: "abcdef"
13517 let runs = vec![
13518 generate_test_run(2, text_color), // ab
13519 generate_test_run(4, text_color), // cdef
13520 ];
13521 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
13522 // new splits "ab", "cd", "ef"
13523 assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![2, 2, 2]);
13524 assert_eq!(out[0].color, text_color);
13525 assert_eq!(out[1].color, adjusted_bg1);
13526 assert_eq!(out[2].color, adjusted_bg2);
13527
13528 // for text: "ghijklmn"
13529 let runs = vec![
13530 generate_test_run(3, text_color), // ghi
13531 generate_test_run(2, text_color), // jk
13532 generate_test_run(3, text_color), // lmn
13533 ];
13534 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 6); // 2 + 4 from first run
13535 // new splits "gh", "i", "jk", "l", "mn"
13536 assert_eq!(
13537 out.iter().map(|r| r.len).collect::<Vec<_>>(),
13538 vec![2, 1, 2, 1, 2]
13539 );
13540 assert_eq!(out[0].color, adjusted_bg2);
13541 assert_eq!(out[1].color, text_color);
13542 assert_eq!(out[2].color, adjusted_bg1);
13543 assert_eq!(out[3].color, text_color);
13544 assert_eq!(out[4].color, adjusted_bg2);
13545
13546 // for text: "opqrs"
13547 let runs = vec![
13548 generate_test_run(1, text_color), // o
13549 generate_test_run(4, text_color), // pqrs
13550 ];
13551 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 14); // 6 + 3 + 2 + 3 from first two runs
13552 // new splits "o", "p", "qr", "s"
13553 assert_eq!(
13554 out.iter().map(|r| r.len).collect::<Vec<_>>(),
13555 vec![1, 1, 2, 1]
13556 );
13557 assert_eq!(out[0].color, adjusted_bg2);
13558 assert_eq!(out[1].color, adjusted_bg2);
13559 assert_eq!(out[2].color, text_color);
13560 assert_eq!(out[3].color, adjusted_bg1);
13561 }
13562 }
13563
13564 #[test]
13565 fn test_spacer_pattern_period() {
13566 // line height is smaller than target height, so we just return half the line height
13567 assert_eq!(EditorElement::spacer_pattern_period(10.0, 20.0), 5.0);
13568
13569 // line height is exactly half the target height, perfect match
13570 assert_eq!(EditorElement::spacer_pattern_period(20.0, 10.0), 10.0);
13571
13572 // line height is close to half the target height
13573 assert_eq!(EditorElement::spacer_pattern_period(20.0, 9.0), 10.0);
13574
13575 // line height is close to 1/4 the target height
13576 assert_eq!(EditorElement::spacer_pattern_period(20.0, 4.8), 5.0);
13577 }
13578
13579 #[gpui::test(iterations = 100)]
13580 fn test_random_spacer_pattern_period(mut rng: StdRng) {
13581 let line_height = rng.next_u32() as f32;
13582 let target_height = rng.next_u32() as f32;
13583
13584 let result = EditorElement::spacer_pattern_period(line_height, target_height);
13585
13586 let k = line_height / result;
13587 assert!(k - k.round() < 0.0000001); // approximately integer
13588 assert!((k.round() as u32).is_multiple_of(2));
13589 }
13590
13591 #[test]
13592 fn test_calculate_wrap_width() {
13593 let editor_width = px(800.0);
13594 let em_width = px(8.0);
13595
13596 assert_eq!(
13597 calculate_wrap_width(SoftWrap::GitDiff, editor_width, em_width),
13598 None,
13599 );
13600
13601 assert_eq!(
13602 calculate_wrap_width(SoftWrap::None, editor_width, em_width),
13603 Some(px((MAX_LINE_LEN as f32 / 2.0 * 8.0).ceil())),
13604 );
13605
13606 assert_eq!(
13607 calculate_wrap_width(SoftWrap::EditorWidth, editor_width, em_width),
13608 Some(px(800.0)),
13609 );
13610
13611 assert_eq!(
13612 calculate_wrap_width(SoftWrap::Column(72), editor_width, em_width),
13613 Some(px((72.0 * 8.0_f32).ceil())),
13614 );
13615
13616 assert_eq!(
13617 calculate_wrap_width(SoftWrap::Bounded(72), editor_width, em_width),
13618 Some(px((72.0 * 8.0_f32).ceil())),
13619 );
13620 assert_eq!(
13621 calculate_wrap_width(SoftWrap::Bounded(200), px(400.0), em_width),
13622 Some(px(400.0)),
13623 );
13624 }
13625}