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