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