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