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