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