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 ..Default::default()
1705 }],
1706 None,
1707 )
1708 })
1709 } else {
1710 None
1711 };
1712
1713 let x = cursor_character_x - scroll_pixel_position.x.into();
1714 let y = ((cursor_position.row().as_f64() - scroll_position.y)
1715 * ScrollPixelOffset::from(line_height))
1716 .into();
1717 if selection.is_newest {
1718 editor.pixel_position_of_newest_cursor = Some(point(
1719 text_hitbox.origin.x + x + block_width / 2.,
1720 text_hitbox.origin.y + y + line_height / 2.,
1721 ));
1722
1723 if autoscroll_containing_element {
1724 let top = text_hitbox.origin.y
1725 + ((cursor_position.row().as_f64() - scroll_position.y - 3.)
1726 .max(0.)
1727 * ScrollPixelOffset::from(line_height))
1728 .into();
1729 let left = text_hitbox.origin.x
1730 + ((cursor_position.column() as ScrollOffset
1731 - scroll_position.x
1732 - 3.)
1733 .max(0.)
1734 * ScrollPixelOffset::from(em_width))
1735 .into();
1736
1737 let bottom = text_hitbox.origin.y
1738 + ((cursor_position.row().as_f64() - scroll_position.y + 4.)
1739 * ScrollPixelOffset::from(line_height))
1740 .into();
1741 let right = text_hitbox.origin.x
1742 + ((cursor_position.column() as ScrollOffset - scroll_position.x
1743 + 4.)
1744 * ScrollPixelOffset::from(em_width))
1745 .into();
1746
1747 autoscroll_bounds =
1748 Some(Bounds::from_corners(point(left, top), point(right, bottom)))
1749 }
1750 }
1751
1752 let mut cursor = CursorLayout {
1753 color: player_color.cursor,
1754 block_width,
1755 origin: point(x, y),
1756 line_height,
1757 shape: selection.cursor_shape,
1758 block_text,
1759 cursor_name: None,
1760 };
1761 let cursor_name = selection.user_name.clone().map(|name| CursorName {
1762 string: name,
1763 color: self.style.background,
1764 is_top_row: cursor_position.row().0 == 0,
1765 });
1766 cursor.layout(content_origin, cursor_name, window, cx);
1767 cursors.push(cursor);
1768 }
1769 }
1770
1771 cursors
1772 });
1773
1774 if let Some(bounds) = autoscroll_bounds {
1775 window.request_autoscroll(bounds);
1776 }
1777
1778 cursor_layouts
1779 }
1780
1781 fn layout_scrollbars(
1782 &self,
1783 snapshot: &EditorSnapshot,
1784 scrollbar_layout_information: &ScrollbarLayoutInformation,
1785 content_offset: gpui::Point<Pixels>,
1786 scroll_position: gpui::Point<ScrollOffset>,
1787 non_visible_cursors: bool,
1788 right_margin: Pixels,
1789 editor_width: Pixels,
1790 window: &mut Window,
1791 cx: &mut App,
1792 ) -> Option<EditorScrollbars> {
1793 let show_scrollbars = self.editor.read(cx).show_scrollbars;
1794 if (!show_scrollbars.horizontal && !show_scrollbars.vertical)
1795 || self.style.scrollbar_width.is_zero()
1796 {
1797 return None;
1798 }
1799
1800 // If a drag took place after we started dragging the scrollbar,
1801 // cancel the scrollbar drag.
1802 if cx.has_active_drag() {
1803 self.editor.update(cx, |editor, cx| {
1804 editor.scroll_manager.reset_scrollbar_state(cx)
1805 });
1806 }
1807
1808 let editor_settings = EditorSettings::get_global(cx);
1809 let scrollbar_settings = editor_settings.scrollbar;
1810 let show_scrollbars = match scrollbar_settings.show {
1811 ShowScrollbar::Auto => {
1812 let editor = self.editor.read(cx);
1813 let is_singleton = editor.buffer_kind(cx) == ItemBufferKind::Singleton;
1814 // Git
1815 (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot().has_diff_hunks())
1816 ||
1817 // Buffer Search Results
1818 (is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
1819 ||
1820 // Selected Text Occurrences
1821 (is_singleton && scrollbar_settings.selected_text && editor.has_background_highlights::<SelectedTextHighlight>())
1822 ||
1823 // Selected Symbol Occurrences
1824 (is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
1825 ||
1826 // Diagnostics
1827 (is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot().has_diagnostics())
1828 ||
1829 // Cursors out of sight
1830 non_visible_cursors
1831 ||
1832 // Scrollmanager
1833 editor.scroll_manager.scrollbars_visible()
1834 }
1835 ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
1836 ShowScrollbar::Always => true,
1837 ShowScrollbar::Never => return None,
1838 };
1839
1840 // The horizontal scrollbar is usually slightly offset to align nicely with
1841 // indent guides. However, this offset is not needed if indent guides are
1842 // disabled for the current editor.
1843 let content_offset = self
1844 .editor
1845 .read(cx)
1846 .show_indent_guides
1847 .is_none_or(|should_show| should_show)
1848 .then_some(content_offset)
1849 .unwrap_or_default();
1850
1851 Some(EditorScrollbars::from_scrollbar_axes(
1852 ScrollbarAxes {
1853 horizontal: scrollbar_settings.axes.horizontal
1854 && self.editor.read(cx).show_scrollbars.horizontal,
1855 vertical: scrollbar_settings.axes.vertical
1856 && self.editor.read(cx).show_scrollbars.vertical,
1857 },
1858 scrollbar_layout_information,
1859 content_offset,
1860 scroll_position,
1861 self.style.scrollbar_width,
1862 right_margin,
1863 editor_width,
1864 show_scrollbars,
1865 self.editor.read(cx).scroll_manager.active_scrollbar_state(),
1866 window,
1867 ))
1868 }
1869
1870 fn layout_minimap(
1871 &self,
1872 snapshot: &EditorSnapshot,
1873 minimap_width: Pixels,
1874 scroll_position: gpui::Point<f64>,
1875 scrollbar_layout_information: &ScrollbarLayoutInformation,
1876 scrollbar_layout: Option<&EditorScrollbars>,
1877 window: &mut Window,
1878 cx: &mut App,
1879 ) -> Option<MinimapLayout> {
1880 let minimap_editor = self.editor.read(cx).minimap().cloned()?;
1881
1882 let minimap_settings = EditorSettings::get_global(cx).minimap;
1883
1884 if minimap_settings.on_active_editor() {
1885 let active_editor = self.editor.read(cx).workspace().and_then(|ws| {
1886 ws.read(cx)
1887 .active_pane()
1888 .read(cx)
1889 .active_item()
1890 .and_then(|i| i.act_as::<Editor>(cx))
1891 });
1892 if active_editor.is_some_and(|e| e != self.editor) {
1893 return None;
1894 }
1895 }
1896
1897 if !snapshot.mode.is_full()
1898 || minimap_width.is_zero()
1899 || matches!(
1900 minimap_settings.show,
1901 ShowMinimap::Auto if scrollbar_layout.is_none_or(|layout| !layout.visible)
1902 )
1903 {
1904 return None;
1905 }
1906
1907 const MINIMAP_AXIS: ScrollbarAxis = ScrollbarAxis::Vertical;
1908
1909 let ScrollbarLayoutInformation {
1910 editor_bounds,
1911 scroll_range,
1912 glyph_grid_cell,
1913 } = scrollbar_layout_information;
1914
1915 let line_height = glyph_grid_cell.height;
1916 let scroll_position = scroll_position.along(MINIMAP_AXIS);
1917
1918 let top_right_anchor = scrollbar_layout
1919 .and_then(|layout| layout.vertical.as_ref())
1920 .map(|vertical_scrollbar| vertical_scrollbar.hitbox.origin)
1921 .unwrap_or_else(|| editor_bounds.top_right());
1922
1923 let thumb_state = self
1924 .editor
1925 .read_with(cx, |editor, _| editor.scroll_manager.minimap_thumb_state());
1926
1927 let show_thumb = match minimap_settings.thumb {
1928 MinimapThumb::Always => true,
1929 MinimapThumb::Hover => thumb_state.is_some(),
1930 };
1931
1932 let minimap_bounds = Bounds::from_corner_and_size(
1933 Corner::TopRight,
1934 top_right_anchor,
1935 size(minimap_width, editor_bounds.size.height),
1936 );
1937 let minimap_line_height = self.get_minimap_line_height(
1938 minimap_editor
1939 .read(cx)
1940 .text_style_refinement
1941 .as_ref()
1942 .and_then(|refinement| refinement.font_size)
1943 .unwrap_or(MINIMAP_FONT_SIZE),
1944 window,
1945 cx,
1946 );
1947 let minimap_height = minimap_bounds.size.height;
1948
1949 let visible_editor_lines = (editor_bounds.size.height / line_height) as f64;
1950 let total_editor_lines = (scroll_range.height / line_height) as f64;
1951 let minimap_lines = (minimap_height / minimap_line_height) as f64;
1952
1953 let minimap_scroll_top = MinimapLayout::calculate_minimap_top_offset(
1954 total_editor_lines,
1955 visible_editor_lines,
1956 minimap_lines,
1957 scroll_position,
1958 );
1959
1960 let layout = ScrollbarLayout::for_minimap(
1961 window.insert_hitbox(minimap_bounds, HitboxBehavior::Normal),
1962 visible_editor_lines,
1963 total_editor_lines,
1964 minimap_line_height,
1965 scroll_position,
1966 minimap_scroll_top,
1967 show_thumb,
1968 )
1969 .with_thumb_state(thumb_state);
1970
1971 minimap_editor.update(cx, |editor, cx| {
1972 editor.set_scroll_position(point(0., minimap_scroll_top), window, cx)
1973 });
1974
1975 // Required for the drop shadow to be visible
1976 const PADDING_OFFSET: Pixels = px(4.);
1977
1978 let mut minimap = div()
1979 .size_full()
1980 .shadow_xs()
1981 .px(PADDING_OFFSET)
1982 .child(minimap_editor)
1983 .into_any_element();
1984
1985 let extended_bounds = minimap_bounds.extend(Edges {
1986 right: PADDING_OFFSET,
1987 left: PADDING_OFFSET,
1988 ..Default::default()
1989 });
1990 minimap.layout_as_root(extended_bounds.size.into(), window, cx);
1991 window.with_absolute_element_offset(extended_bounds.origin, |window| {
1992 minimap.prepaint(window, cx)
1993 });
1994
1995 Some(MinimapLayout {
1996 minimap,
1997 thumb_layout: layout,
1998 thumb_border_style: minimap_settings.thumb_border,
1999 minimap_line_height,
2000 minimap_scroll_top,
2001 max_scroll_top: total_editor_lines,
2002 })
2003 }
2004
2005 fn get_minimap_line_height(
2006 &self,
2007 font_size: AbsoluteLength,
2008 window: &mut Window,
2009 cx: &mut App,
2010 ) -> Pixels {
2011 let rem_size = self.rem_size(cx).unwrap_or(window.rem_size());
2012 let mut text_style = self.style.text.clone();
2013 text_style.font_size = font_size;
2014 text_style.line_height_in_pixels(rem_size)
2015 }
2016
2017 fn get_minimap_width(
2018 &self,
2019 minimap_settings: &Minimap,
2020 scrollbars_shown: bool,
2021 text_width: Pixels,
2022 em_width: Pixels,
2023 font_size: Pixels,
2024 rem_size: Pixels,
2025 cx: &App,
2026 ) -> Option<Pixels> {
2027 if minimap_settings.show == ShowMinimap::Auto && !scrollbars_shown {
2028 return None;
2029 }
2030
2031 let minimap_font_size = self.editor.read_with(cx, |editor, cx| {
2032 editor.minimap().map(|minimap_editor| {
2033 minimap_editor
2034 .read(cx)
2035 .text_style_refinement
2036 .as_ref()
2037 .and_then(|refinement| refinement.font_size)
2038 .unwrap_or(MINIMAP_FONT_SIZE)
2039 })
2040 })?;
2041
2042 let minimap_em_width = em_width * (minimap_font_size.to_pixels(rem_size) / font_size);
2043
2044 let minimap_width = (text_width * MinimapLayout::MINIMAP_WIDTH_PCT)
2045 .min(minimap_em_width * minimap_settings.max_width_columns.get() as f32);
2046
2047 (minimap_width >= minimap_em_width * MinimapLayout::MINIMAP_MIN_WIDTH_COLUMNS)
2048 .then_some(minimap_width)
2049 }
2050
2051 fn prepaint_crease_toggles(
2052 &self,
2053 crease_toggles: &mut [Option<AnyElement>],
2054 line_height: Pixels,
2055 gutter_dimensions: &GutterDimensions,
2056 gutter_settings: crate::editor_settings::Gutter,
2057 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
2058 gutter_hitbox: &Hitbox,
2059 window: &mut Window,
2060 cx: &mut App,
2061 ) {
2062 for (ix, crease_toggle) in crease_toggles.iter_mut().enumerate() {
2063 if let Some(crease_toggle) = crease_toggle {
2064 debug_assert!(gutter_settings.folds);
2065 let available_space = size(
2066 AvailableSpace::MinContent,
2067 AvailableSpace::Definite(line_height * 0.55),
2068 );
2069 let crease_toggle_size = crease_toggle.layout_as_root(available_space, window, cx);
2070
2071 let position = point(
2072 gutter_dimensions.width - gutter_dimensions.right_padding,
2073 ix as f32 * line_height
2074 - (scroll_pixel_position.y % ScrollPixelOffset::from(line_height)).into(),
2075 );
2076 let centering_offset = point(
2077 (gutter_dimensions.fold_area_width() - crease_toggle_size.width) / 2.,
2078 (line_height - crease_toggle_size.height) / 2.,
2079 );
2080 let origin = gutter_hitbox.origin + position + centering_offset;
2081 crease_toggle.prepaint_as_root(origin, available_space, window, cx);
2082 }
2083 }
2084 }
2085
2086 fn prepaint_expand_toggles(
2087 &self,
2088 expand_toggles: &mut [Option<(AnyElement, gpui::Point<Pixels>)>],
2089 window: &mut Window,
2090 cx: &mut App,
2091 ) {
2092 for (expand_toggle, origin) in expand_toggles.iter_mut().flatten() {
2093 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
2094 expand_toggle.layout_as_root(available_space, window, cx);
2095 expand_toggle.prepaint_as_root(*origin, available_space, window, cx);
2096 }
2097 }
2098
2099 fn prepaint_crease_trailers(
2100 &self,
2101 trailers: Vec<Option<AnyElement>>,
2102 lines: &[LineWithInvisibles],
2103 line_height: Pixels,
2104 content_origin: gpui::Point<Pixels>,
2105 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
2106 em_width: Pixels,
2107 window: &mut Window,
2108 cx: &mut App,
2109 ) -> Vec<Option<CreaseTrailerLayout>> {
2110 trailers
2111 .into_iter()
2112 .enumerate()
2113 .map(|(ix, element)| {
2114 let mut element = element?;
2115 let available_space = size(
2116 AvailableSpace::MinContent,
2117 AvailableSpace::Definite(line_height),
2118 );
2119 let size = element.layout_as_root(available_space, window, cx);
2120
2121 let line = &lines[ix];
2122 let padding = if line.width == Pixels::ZERO {
2123 Pixels::ZERO
2124 } else {
2125 4. * em_width
2126 };
2127 let position = point(
2128 Pixels::from(scroll_pixel_position.x) + line.width + padding,
2129 ix as f32 * line_height
2130 - (scroll_pixel_position.y % ScrollPixelOffset::from(line_height)).into(),
2131 );
2132 let centering_offset = point(px(0.), (line_height - size.height) / 2.);
2133 let origin = content_origin + position + centering_offset;
2134 element.prepaint_as_root(origin, available_space, window, cx);
2135 Some(CreaseTrailerLayout {
2136 element,
2137 bounds: Bounds::new(origin, size),
2138 })
2139 })
2140 .collect()
2141 }
2142
2143 // Folds contained in a hunk are ignored apart from shrinking visual size
2144 // If a fold contains any hunks then that fold line is marked as modified
2145 fn layout_gutter_diff_hunks(
2146 &self,
2147 line_height: Pixels,
2148 gutter_hitbox: &Hitbox,
2149 display_rows: Range<DisplayRow>,
2150 snapshot: &EditorSnapshot,
2151 window: &mut Window,
2152 cx: &mut App,
2153 ) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
2154 let folded_buffers = self.editor.read(cx).folded_buffers(cx);
2155 let mut display_hunks = snapshot
2156 .display_diff_hunks_for_rows(display_rows, folded_buffers)
2157 .map(|hunk| (hunk, None))
2158 .collect::<Vec<_>>();
2159 let git_gutter_setting = ProjectSettings::get_global(cx).git.git_gutter;
2160 if let GitGutterSetting::TrackedFiles = git_gutter_setting {
2161 for (hunk, hitbox) in &mut display_hunks {
2162 if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
2163 let hunk_bounds =
2164 Self::diff_hunk_bounds(snapshot, line_height, gutter_hitbox.bounds, hunk);
2165 *hitbox = Some(window.insert_hitbox(hunk_bounds, HitboxBehavior::BlockMouse));
2166 }
2167 }
2168 }
2169
2170 display_hunks
2171 }
2172
2173 fn layout_inline_diagnostics(
2174 &self,
2175 line_layouts: &[LineWithInvisibles],
2176 crease_trailers: &[Option<CreaseTrailerLayout>],
2177 row_block_types: &HashMap<DisplayRow, bool>,
2178 content_origin: gpui::Point<Pixels>,
2179 scroll_position: gpui::Point<ScrollOffset>,
2180 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
2181 edit_prediction_popover_origin: Option<gpui::Point<Pixels>>,
2182 start_row: DisplayRow,
2183 end_row: DisplayRow,
2184 line_height: Pixels,
2185 em_width: Pixels,
2186 style: &EditorStyle,
2187 window: &mut Window,
2188 cx: &mut App,
2189 ) -> HashMap<DisplayRow, AnyElement> {
2190 let max_severity = match self
2191 .editor
2192 .read(cx)
2193 .inline_diagnostics_enabled()
2194 .then(|| {
2195 ProjectSettings::get_global(cx)
2196 .diagnostics
2197 .inline
2198 .max_severity
2199 .unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity)
2200 .into_lsp()
2201 })
2202 .flatten()
2203 {
2204 Some(max_severity) => max_severity,
2205 None => return HashMap::default(),
2206 };
2207
2208 let active_diagnostics_group =
2209 if let ActiveDiagnostic::Group(group) = &self.editor.read(cx).active_diagnostics {
2210 Some(group.group_id)
2211 } else {
2212 None
2213 };
2214
2215 let diagnostics_by_rows = self.editor.update(cx, |editor, cx| {
2216 let snapshot = editor.snapshot(window, cx);
2217 editor
2218 .inline_diagnostics
2219 .iter()
2220 .filter(|(_, diagnostic)| diagnostic.severity <= max_severity)
2221 .filter(|(_, diagnostic)| match active_diagnostics_group {
2222 Some(active_diagnostics_group) => {
2223 // Active diagnostics are all shown in the editor already, no need to display them inline
2224 diagnostic.group_id != active_diagnostics_group
2225 }
2226 None => true,
2227 })
2228 .map(|(point, diag)| (point.to_display_point(&snapshot), diag.clone()))
2229 .skip_while(|(point, _)| point.row() < start_row)
2230 .take_while(|(point, _)| point.row() < end_row)
2231 .filter(|(point, _)| !row_block_types.contains_key(&point.row()))
2232 .fold(HashMap::default(), |mut acc, (point, diagnostic)| {
2233 acc.entry(point.row())
2234 .or_insert_with(Vec::new)
2235 .push(diagnostic);
2236 acc
2237 })
2238 });
2239
2240 if diagnostics_by_rows.is_empty() {
2241 return HashMap::default();
2242 }
2243
2244 let severity_to_color = |sev: &lsp::DiagnosticSeverity| match sev {
2245 &lsp::DiagnosticSeverity::ERROR => Color::Error,
2246 &lsp::DiagnosticSeverity::WARNING => Color::Warning,
2247 &lsp::DiagnosticSeverity::INFORMATION => Color::Info,
2248 &lsp::DiagnosticSeverity::HINT => Color::Hint,
2249 _ => Color::Error,
2250 };
2251
2252 let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width;
2253 let min_x = self.column_pixels(
2254 ProjectSettings::get_global(cx)
2255 .diagnostics
2256 .inline
2257 .min_column as usize,
2258 window,
2259 );
2260
2261 let mut elements = HashMap::default();
2262 for (row, mut diagnostics) in diagnostics_by_rows {
2263 diagnostics.sort_by_key(|diagnostic| {
2264 (
2265 diagnostic.severity,
2266 std::cmp::Reverse(diagnostic.is_primary),
2267 diagnostic.start.row,
2268 diagnostic.start.column,
2269 )
2270 });
2271
2272 let Some(diagnostic_to_render) = diagnostics
2273 .iter()
2274 .find(|diagnostic| diagnostic.is_primary)
2275 .or_else(|| diagnostics.first())
2276 else {
2277 continue;
2278 };
2279
2280 let pos_y = content_origin.y + line_height * (row.0 as f64 - scroll_position.y) as f32;
2281
2282 let window_ix = row.0.saturating_sub(start_row.0) as usize;
2283 let pos_x = {
2284 let crease_trailer_layout = &crease_trailers[window_ix];
2285 let line_layout = &line_layouts[window_ix];
2286
2287 let line_end = if let Some(crease_trailer) = crease_trailer_layout {
2288 crease_trailer.bounds.right()
2289 } else {
2290 Pixels::from(
2291 ScrollPixelOffset::from(content_origin.x + line_layout.width)
2292 - scroll_pixel_position.x,
2293 )
2294 };
2295
2296 let padded_line = line_end + padding;
2297 let min_start = Pixels::from(
2298 ScrollPixelOffset::from(content_origin.x + min_x) - scroll_pixel_position.x,
2299 );
2300
2301 cmp::max(padded_line, min_start)
2302 };
2303
2304 let behind_edit_prediction_popover = edit_prediction_popover_origin
2305 .as_ref()
2306 .is_some_and(|edit_prediction_popover_origin| {
2307 (pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y)
2308 });
2309 let opacity = if behind_edit_prediction_popover {
2310 0.5
2311 } else {
2312 1.0
2313 };
2314
2315 let mut element = h_flex()
2316 .id(("diagnostic", row.0))
2317 .h(line_height)
2318 .w_full()
2319 .px_1()
2320 .rounded_xs()
2321 .opacity(opacity)
2322 .bg(severity_to_color(&diagnostic_to_render.severity)
2323 .color(cx)
2324 .opacity(0.05))
2325 .text_color(severity_to_color(&diagnostic_to_render.severity).color(cx))
2326 .text_sm()
2327 .font_family(style.text.font().family)
2328 .child(diagnostic_to_render.message.clone())
2329 .into_any();
2330
2331 element.prepaint_as_root(point(pos_x, pos_y), AvailableSpace::min_size(), window, cx);
2332
2333 elements.insert(row, element);
2334 }
2335
2336 elements
2337 }
2338
2339 fn layout_inline_code_actions(
2340 &self,
2341 display_point: DisplayPoint,
2342 content_origin: gpui::Point<Pixels>,
2343 scroll_position: gpui::Point<ScrollOffset>,
2344 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
2345 line_height: Pixels,
2346 snapshot: &EditorSnapshot,
2347 window: &mut Window,
2348 cx: &mut App,
2349 ) -> Option<AnyElement> {
2350 if !snapshot
2351 .show_code_actions
2352 .unwrap_or(EditorSettings::get_global(cx).inline_code_actions)
2353 {
2354 return None;
2355 }
2356
2357 let icon_size = ui::IconSize::XSmall;
2358 let mut button = self.editor.update(cx, |editor, cx| {
2359 editor.available_code_actions.as_ref()?;
2360 let active = editor
2361 .context_menu
2362 .borrow()
2363 .as_ref()
2364 .and_then(|menu| {
2365 if let crate::CodeContextMenu::CodeActions(CodeActionsMenu {
2366 deployed_from,
2367 ..
2368 }) = menu
2369 {
2370 deployed_from.as_ref()
2371 } else {
2372 None
2373 }
2374 })
2375 .is_some_and(|source| matches!(source, CodeActionSource::Indicator(..)));
2376 Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx))
2377 })?;
2378
2379 let buffer_point = display_point.to_point(&snapshot.display_snapshot);
2380
2381 // do not show code action for folded line
2382 if snapshot.is_line_folded(MultiBufferRow(buffer_point.row)) {
2383 return None;
2384 }
2385
2386 // do not show code action for blank line with cursor
2387 let line_indent = snapshot
2388 .display_snapshot
2389 .buffer_snapshot()
2390 .line_indent_for_row(MultiBufferRow(buffer_point.row));
2391 if line_indent.is_line_blank() {
2392 return None;
2393 }
2394
2395 const INLINE_SLOT_CHAR_LIMIT: u32 = 4;
2396 const MAX_ALTERNATE_DISTANCE: u32 = 8;
2397
2398 let excerpt_id = snapshot
2399 .display_snapshot
2400 .buffer_snapshot()
2401 .excerpt_containing(buffer_point..buffer_point)
2402 .map(|excerpt| excerpt.id());
2403
2404 let is_valid_row = |row_candidate: u32| -> bool {
2405 // move to other row if folded row
2406 if snapshot.is_line_folded(MultiBufferRow(row_candidate)) {
2407 return false;
2408 }
2409 if buffer_point.row == row_candidate {
2410 // move to other row if cursor is in slot
2411 if buffer_point.column < INLINE_SLOT_CHAR_LIMIT {
2412 return false;
2413 }
2414 } else {
2415 let candidate_point = MultiBufferPoint {
2416 row: row_candidate,
2417 column: 0,
2418 };
2419 let candidate_excerpt_id = snapshot
2420 .display_snapshot
2421 .buffer_snapshot()
2422 .excerpt_containing(candidate_point..candidate_point)
2423 .map(|excerpt| excerpt.id());
2424 // move to other row if different excerpt
2425 if excerpt_id != candidate_excerpt_id {
2426 return false;
2427 }
2428 }
2429 let line_indent = snapshot
2430 .display_snapshot
2431 .buffer_snapshot()
2432 .line_indent_for_row(MultiBufferRow(row_candidate));
2433 // use this row if it's blank
2434 if line_indent.is_line_blank() {
2435 true
2436 } else {
2437 // use this row if code starts after slot
2438 let indent_size = snapshot
2439 .display_snapshot
2440 .buffer_snapshot()
2441 .indent_size_for_line(MultiBufferRow(row_candidate));
2442 indent_size.len >= INLINE_SLOT_CHAR_LIMIT
2443 }
2444 };
2445
2446 let new_buffer_row = if is_valid_row(buffer_point.row) {
2447 Some(buffer_point.row)
2448 } else {
2449 let max_row = snapshot.display_snapshot.buffer_snapshot().max_point().row;
2450 (1..=MAX_ALTERNATE_DISTANCE).find_map(|offset| {
2451 let row_above = buffer_point.row.saturating_sub(offset);
2452 let row_below = buffer_point.row + offset;
2453 if row_above != buffer_point.row && is_valid_row(row_above) {
2454 Some(row_above)
2455 } else if row_below <= max_row && is_valid_row(row_below) {
2456 Some(row_below)
2457 } else {
2458 None
2459 }
2460 })
2461 }?;
2462
2463 let new_display_row = snapshot
2464 .display_snapshot
2465 .point_to_display_point(
2466 Point {
2467 row: new_buffer_row,
2468 column: buffer_point.column,
2469 },
2470 text::Bias::Left,
2471 )
2472 .row();
2473
2474 let start_y = content_origin.y
2475 + (((new_display_row.as_f64() - scroll_position.y) as f32) * line_height)
2476 + (line_height / 2.0)
2477 - (icon_size.square(window, cx) / 2.);
2478 let start_x = (ScrollPixelOffset::from(content_origin.x) - scroll_pixel_position.x
2479 + ScrollPixelOffset::from(window.rem_size() * 0.1))
2480 .into();
2481
2482 let absolute_offset = gpui::point(start_x, start_y);
2483 button.layout_as_root(gpui::AvailableSpace::min_size(), window, cx);
2484 button.prepaint_as_root(
2485 absolute_offset,
2486 gpui::AvailableSpace::min_size(),
2487 window,
2488 cx,
2489 );
2490 Some(button)
2491 }
2492
2493 fn layout_inline_blame(
2494 &self,
2495 display_row: DisplayRow,
2496 row_info: &RowInfo,
2497 line_layout: &LineWithInvisibles,
2498 crease_trailer: Option<&CreaseTrailerLayout>,
2499 em_width: Pixels,
2500 content_origin: gpui::Point<Pixels>,
2501 scroll_position: gpui::Point<ScrollOffset>,
2502 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
2503 line_height: Pixels,
2504 text_hitbox: &Hitbox,
2505 window: &mut Window,
2506 cx: &mut App,
2507 ) -> Option<InlineBlameLayout> {
2508 if !self
2509 .editor
2510 .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx))
2511 {
2512 return None;
2513 }
2514
2515 let editor = self.editor.read(cx);
2516 let blame = editor.blame.clone()?;
2517 let padding = {
2518 const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.;
2519
2520 let mut padding = ProjectSettings::get_global(cx).git.inline_blame.padding as f32;
2521
2522 if let Some(edit_prediction) = editor.active_edit_prediction.as_ref()
2523 && let EditPrediction::Edit {
2524 display_mode: EditDisplayMode::TabAccept,
2525 ..
2526 } = &edit_prediction.completion
2527 {
2528 padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS
2529 }
2530
2531 padding * em_width
2532 };
2533
2534 let (buffer_id, entry) = blame
2535 .update(cx, |blame, cx| {
2536 blame.blame_for_rows(&[*row_info], cx).next()
2537 })
2538 .flatten()?;
2539
2540 let mut element = render_inline_blame_entry(entry.clone(), &self.style, cx)?;
2541
2542 let start_y =
2543 content_origin.y + line_height * ((display_row.as_f64() - scroll_position.y) as f32);
2544
2545 let start_x = {
2546 let line_end = if let Some(crease_trailer) = crease_trailer {
2547 crease_trailer.bounds.right()
2548 } else {
2549 Pixels::from(
2550 ScrollPixelOffset::from(content_origin.x + line_layout.width)
2551 - scroll_pixel_position.x,
2552 )
2553 };
2554
2555 let padded_line_end = line_end + padding;
2556
2557 let min_column_in_pixels = self.column_pixels(
2558 ProjectSettings::get_global(cx).git.inline_blame.min_column as usize,
2559 window,
2560 );
2561 let min_start = Pixels::from(
2562 ScrollPixelOffset::from(content_origin.x + min_column_in_pixels)
2563 - scroll_pixel_position.x,
2564 );
2565
2566 cmp::max(padded_line_end, min_start)
2567 };
2568
2569 let absolute_offset = point(start_x, start_y);
2570 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
2571 let bounds = Bounds::new(absolute_offset, size);
2572
2573 self.layout_blame_entry_popover(
2574 entry.clone(),
2575 blame,
2576 line_height,
2577 text_hitbox,
2578 row_info.buffer_id?,
2579 window,
2580 cx,
2581 );
2582
2583 element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
2584
2585 Some(InlineBlameLayout {
2586 element,
2587 bounds,
2588 buffer_id,
2589 entry,
2590 })
2591 }
2592
2593 fn layout_blame_entry_popover(
2594 &self,
2595 blame_entry: BlameEntry,
2596 blame: Entity<GitBlame>,
2597 line_height: Pixels,
2598 text_hitbox: &Hitbox,
2599 buffer: BufferId,
2600 window: &mut Window,
2601 cx: &mut App,
2602 ) {
2603 let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| {
2604 editor
2605 .inline_blame_popover
2606 .as_ref()
2607 .map(|state| (state.popover_state.clone(), state.position))
2608 }) else {
2609 return;
2610 };
2611
2612 let workspace = self
2613 .editor
2614 .read_with(cx, |editor, _| editor.workspace().map(|w| w.downgrade()));
2615
2616 let maybe_element = workspace.and_then(|workspace| {
2617 render_blame_entry_popover(
2618 blame_entry,
2619 popover_state.scroll_handle,
2620 popover_state.commit_message,
2621 popover_state.markdown,
2622 workspace,
2623 &blame,
2624 buffer,
2625 window,
2626 cx,
2627 )
2628 });
2629
2630 if let Some(mut element) = maybe_element {
2631 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
2632 let overall_height = size.height + HOVER_POPOVER_GAP;
2633 let popover_origin = if target_point.y > overall_height {
2634 point(target_point.x, target_point.y - size.height)
2635 } else {
2636 point(
2637 target_point.x,
2638 target_point.y + line_height + HOVER_POPOVER_GAP,
2639 )
2640 };
2641
2642 let horizontal_offset = (text_hitbox.top_right().x
2643 - POPOVER_RIGHT_OFFSET
2644 - (popover_origin.x + size.width))
2645 .min(Pixels::ZERO);
2646
2647 let origin = point(popover_origin.x + horizontal_offset, popover_origin.y);
2648 let popover_bounds = Bounds::new(origin, size);
2649
2650 self.editor.update(cx, |editor, _| {
2651 if let Some(state) = &mut editor.inline_blame_popover {
2652 state.popover_bounds = Some(popover_bounds);
2653 }
2654 });
2655
2656 window.defer_draw(element, origin, 2);
2657 }
2658 }
2659
2660 fn layout_blame_entries(
2661 &self,
2662 buffer_rows: &[RowInfo],
2663 em_width: Pixels,
2664 scroll_position: gpui::Point<ScrollOffset>,
2665 line_height: Pixels,
2666 gutter_hitbox: &Hitbox,
2667 max_width: Option<Pixels>,
2668 window: &mut Window,
2669 cx: &mut App,
2670 ) -> Option<Vec<AnyElement>> {
2671 if !self
2672 .editor
2673 .update(cx, |editor, cx| editor.render_git_blame_gutter(cx))
2674 {
2675 return None;
2676 }
2677
2678 let blame = self.editor.read(cx).blame.clone()?;
2679 let workspace = self.editor.read(cx).workspace()?;
2680 let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
2681 blame.blame_for_rows(buffer_rows, cx).collect()
2682 });
2683
2684 let width = if let Some(max_width) = max_width {
2685 AvailableSpace::Definite(max_width)
2686 } else {
2687 AvailableSpace::MaxContent
2688 };
2689 let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
2690 let start_x = em_width;
2691
2692 let mut last_used_color: Option<(Hsla, Oid)> = None;
2693 let blame_renderer = cx.global::<GlobalBlameRenderer>().0.clone();
2694
2695 let shaped_lines = blamed_rows
2696 .into_iter()
2697 .enumerate()
2698 .flat_map(|(ix, blame_entry)| {
2699 let (buffer_id, blame_entry) = blame_entry?;
2700 let mut element = render_blame_entry(
2701 ix,
2702 &blame,
2703 blame_entry,
2704 &self.style,
2705 &mut last_used_color,
2706 self.editor.clone(),
2707 workspace.clone(),
2708 buffer_id,
2709 &*blame_renderer,
2710 window,
2711 cx,
2712 )?;
2713
2714 let start_y = ix as f32 * line_height
2715 - Pixels::from(scroll_top % ScrollPixelOffset::from(line_height));
2716 let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
2717
2718 element.prepaint_as_root(
2719 absolute_offset,
2720 size(width, AvailableSpace::MinContent),
2721 window,
2722 cx,
2723 );
2724
2725 Some(element)
2726 })
2727 .collect();
2728
2729 Some(shaped_lines)
2730 }
2731
2732 fn layout_indent_guides(
2733 &self,
2734 content_origin: gpui::Point<Pixels>,
2735 text_origin: gpui::Point<Pixels>,
2736 visible_buffer_range: Range<MultiBufferRow>,
2737 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
2738 line_height: Pixels,
2739 snapshot: &DisplaySnapshot,
2740 window: &mut Window,
2741 cx: &mut App,
2742 ) -> Option<Vec<IndentGuideLayout>> {
2743 let indent_guides = self.editor.update(cx, |editor, cx| {
2744 editor.indent_guides(visible_buffer_range, snapshot, cx)
2745 })?;
2746
2747 let active_indent_guide_indices = self.editor.update(cx, |editor, cx| {
2748 editor
2749 .find_active_indent_guide_indices(&indent_guides, snapshot, window, cx)
2750 .unwrap_or_default()
2751 });
2752
2753 Some(
2754 indent_guides
2755 .into_iter()
2756 .enumerate()
2757 .filter_map(|(i, indent_guide)| {
2758 let single_indent_width =
2759 self.column_pixels(indent_guide.tab_size as usize, window);
2760 let total_width = single_indent_width * indent_guide.depth as f32;
2761 let start_x = Pixels::from(
2762 ScrollOffset::from(content_origin.x + total_width)
2763 - scroll_pixel_position.x,
2764 );
2765 if start_x >= text_origin.x {
2766 let (offset_y, length) = Self::calculate_indent_guide_bounds(
2767 indent_guide.start_row..indent_guide.end_row,
2768 line_height,
2769 snapshot,
2770 );
2771
2772 let start_y = Pixels::from(
2773 ScrollOffset::from(content_origin.y) + offset_y
2774 - scroll_pixel_position.y,
2775 );
2776
2777 Some(IndentGuideLayout {
2778 origin: point(start_x, start_y),
2779 length,
2780 single_indent_width,
2781 depth: indent_guide.depth,
2782 active: active_indent_guide_indices.contains(&i),
2783 settings: indent_guide.settings,
2784 })
2785 } else {
2786 None
2787 }
2788 })
2789 .collect(),
2790 )
2791 }
2792
2793 fn layout_wrap_guides(
2794 &self,
2795 em_advance: Pixels,
2796 scroll_position: gpui::Point<f64>,
2797 content_origin: gpui::Point<Pixels>,
2798 scrollbar_layout: Option<&EditorScrollbars>,
2799 vertical_scrollbar_width: Pixels,
2800 hitbox: &Hitbox,
2801 window: &Window,
2802 cx: &App,
2803 ) -> SmallVec<[(Pixels, bool); 2]> {
2804 let scroll_left = scroll_position.x as f32 * em_advance;
2805 let content_origin = content_origin.x;
2806 let horizontal_offset = content_origin - scroll_left;
2807 let vertical_scrollbar_width = scrollbar_layout
2808 .and_then(|layout| layout.visible.then_some(vertical_scrollbar_width))
2809 .unwrap_or_default();
2810
2811 self.editor
2812 .read(cx)
2813 .wrap_guides(cx)
2814 .into_iter()
2815 .flat_map(|(guide, active)| {
2816 let wrap_position = self.column_pixels(guide, window);
2817 let wrap_guide_x = wrap_position + horizontal_offset;
2818 let display_wrap_guide = wrap_guide_x >= content_origin
2819 && wrap_guide_x <= hitbox.bounds.right() - vertical_scrollbar_width;
2820
2821 display_wrap_guide.then_some((wrap_guide_x, active))
2822 })
2823 .collect()
2824 }
2825
2826 fn calculate_indent_guide_bounds(
2827 row_range: Range<MultiBufferRow>,
2828 line_height: Pixels,
2829 snapshot: &DisplaySnapshot,
2830 ) -> (f64, gpui::Pixels) {
2831 let start_point = Point::new(row_range.start.0, 0);
2832 let end_point = Point::new(row_range.end.0, 0);
2833
2834 let row_range = start_point.to_display_point(snapshot).row()
2835 ..end_point.to_display_point(snapshot).row();
2836
2837 let mut prev_line = start_point;
2838 prev_line.row = prev_line.row.saturating_sub(1);
2839 let prev_line = prev_line.to_display_point(snapshot).row();
2840
2841 let mut cons_line = end_point;
2842 cons_line.row += 1;
2843 let cons_line = cons_line.to_display_point(snapshot).row();
2844
2845 let mut offset_y = row_range.start.as_f64() * f64::from(line_height);
2846 let mut length = (cons_line.0.saturating_sub(row_range.start.0)) as f32 * line_height;
2847
2848 // If we are at the end of the buffer, ensure that the indent guide extends to the end of the line.
2849 if row_range.end == cons_line {
2850 length += line_height;
2851 }
2852
2853 // If there is a block (e.g. diagnostic) in between the start of the indent guide and the line above,
2854 // we want to extend the indent guide to the start of the block.
2855 let mut block_height = 0;
2856 let mut block_offset = 0;
2857 let mut found_excerpt_header = false;
2858 for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
2859 if matches!(
2860 block,
2861 Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
2862 ) {
2863 found_excerpt_header = true;
2864 break;
2865 }
2866 block_offset += block.height();
2867 block_height += block.height();
2868 }
2869 if !found_excerpt_header {
2870 offset_y -= block_offset as f64 * f64::from(line_height);
2871 length += block_height as f32 * line_height;
2872 }
2873
2874 // If there is a block (e.g. diagnostic) at the end of an multibuffer excerpt,
2875 // we want to ensure that the indent guide stops before the excerpt header.
2876 let mut block_height = 0;
2877 let mut found_excerpt_header = false;
2878 for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
2879 if matches!(
2880 block,
2881 Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
2882 ) {
2883 found_excerpt_header = true;
2884 }
2885 block_height += block.height();
2886 }
2887 if found_excerpt_header {
2888 length -= block_height as f32 * line_height;
2889 }
2890
2891 (offset_y, length)
2892 }
2893
2894 fn layout_breakpoints(
2895 &self,
2896 line_height: Pixels,
2897 range: Range<DisplayRow>,
2898 scroll_position: gpui::Point<ScrollOffset>,
2899 gutter_dimensions: &GutterDimensions,
2900 gutter_hitbox: &Hitbox,
2901 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
2902 snapshot: &EditorSnapshot,
2903 breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
2904 row_infos: &[RowInfo],
2905 window: &mut Window,
2906 cx: &mut App,
2907 ) -> Vec<AnyElement> {
2908 self.editor.update(cx, |editor, cx| {
2909 breakpoints
2910 .into_iter()
2911 .filter_map(|(display_row, (text_anchor, bp, state))| {
2912 if row_infos
2913 .get((display_row.0.saturating_sub(range.start.0)) as usize)
2914 .is_some_and(|row_info| {
2915 row_info.expand_info.is_some()
2916 || row_info
2917 .diff_status
2918 .is_some_and(|status| status.is_deleted())
2919 })
2920 {
2921 return None;
2922 }
2923
2924 if range.start > display_row || range.end < display_row {
2925 return None;
2926 }
2927
2928 let row =
2929 MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(snapshot).row);
2930 if snapshot.is_line_folded(row) {
2931 return None;
2932 }
2933
2934 let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, cx);
2935
2936 let button = prepaint_gutter_button(
2937 button,
2938 display_row,
2939 line_height,
2940 gutter_dimensions,
2941 scroll_position,
2942 gutter_hitbox,
2943 display_hunks,
2944 window,
2945 cx,
2946 );
2947 Some(button)
2948 })
2949 .collect_vec()
2950 })
2951 }
2952
2953 #[allow(clippy::too_many_arguments)]
2954 fn layout_run_indicators(
2955 &self,
2956 line_height: Pixels,
2957 range: Range<DisplayRow>,
2958 row_infos: &[RowInfo],
2959 scroll_position: gpui::Point<ScrollOffset>,
2960 gutter_dimensions: &GutterDimensions,
2961 gutter_hitbox: &Hitbox,
2962 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
2963 snapshot: &EditorSnapshot,
2964 breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
2965 window: &mut Window,
2966 cx: &mut App,
2967 ) -> Vec<AnyElement> {
2968 self.editor.update(cx, |editor, cx| {
2969 let active_task_indicator_row =
2970 // TODO: add edit button on the right side of each row in the context menu
2971 if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
2972 deployed_from,
2973 actions,
2974 ..
2975 })) = editor.context_menu.borrow().as_ref()
2976 {
2977 actions
2978 .tasks()
2979 .map(|tasks| tasks.position.to_display_point(snapshot).row())
2980 .or_else(|| match deployed_from {
2981 Some(CodeActionSource::Indicator(row)) => Some(*row),
2982 _ => None,
2983 })
2984 } else {
2985 None
2986 };
2987
2988 let offset_range_start =
2989 snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left);
2990
2991 let offset_range_end =
2992 snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
2993
2994 editor
2995 .tasks
2996 .iter()
2997 .filter_map(|(_, tasks)| {
2998 let multibuffer_point = tasks.offset.to_point(&snapshot.buffer_snapshot());
2999 if multibuffer_point < offset_range_start
3000 || multibuffer_point > offset_range_end
3001 {
3002 return None;
3003 }
3004 let multibuffer_row = MultiBufferRow(multibuffer_point.row);
3005 let buffer_folded = snapshot
3006 .buffer_snapshot()
3007 .buffer_line_for_row(multibuffer_row)
3008 .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
3009 .map(|buffer_id| editor.is_buffer_folded(buffer_id, cx))
3010 .unwrap_or(false);
3011 if buffer_folded {
3012 return None;
3013 }
3014
3015 if snapshot.is_line_folded(multibuffer_row) {
3016 // Skip folded indicators, unless it's the starting line of a fold.
3017 if multibuffer_row
3018 .0
3019 .checked_sub(1)
3020 .is_some_and(|previous_row| {
3021 snapshot.is_line_folded(MultiBufferRow(previous_row))
3022 })
3023 {
3024 return None;
3025 }
3026 }
3027
3028 let display_row = multibuffer_point.to_display_point(snapshot).row();
3029 if !range.contains(&display_row) {
3030 return None;
3031 }
3032 if row_infos
3033 .get((display_row - range.start).0 as usize)
3034 .is_some_and(|row_info| row_info.expand_info.is_some())
3035 {
3036 return None;
3037 }
3038
3039 let button = editor.render_run_indicator(
3040 &self.style,
3041 Some(display_row) == active_task_indicator_row,
3042 display_row,
3043 breakpoints.remove(&display_row),
3044 cx,
3045 );
3046
3047 let button = prepaint_gutter_button(
3048 button,
3049 display_row,
3050 line_height,
3051 gutter_dimensions,
3052 scroll_position,
3053 gutter_hitbox,
3054 display_hunks,
3055 window,
3056 cx,
3057 );
3058 Some(button)
3059 })
3060 .collect_vec()
3061 })
3062 }
3063
3064 fn layout_expand_toggles(
3065 &self,
3066 gutter_hitbox: &Hitbox,
3067 gutter_dimensions: GutterDimensions,
3068 em_width: Pixels,
3069 line_height: Pixels,
3070 scroll_position: gpui::Point<ScrollOffset>,
3071 buffer_rows: &[RowInfo],
3072 window: &mut Window,
3073 cx: &mut App,
3074 ) -> Vec<Option<(AnyElement, gpui::Point<Pixels>)>> {
3075 if self.editor.read(cx).disable_expand_excerpt_buttons {
3076 return vec![];
3077 }
3078
3079 let editor_font_size = self.style.text.font_size.to_pixels(window.rem_size()) * 1.2;
3080
3081 let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
3082
3083 let max_line_number_length = self
3084 .editor
3085 .read(cx)
3086 .buffer()
3087 .read(cx)
3088 .snapshot(cx)
3089 .widest_line_number()
3090 .ilog10()
3091 + 1;
3092
3093 let git_gutter_width = Self::gutter_strip_width(line_height)
3094 + gutter_dimensions
3095 .git_blame_entries_width
3096 .unwrap_or_default();
3097 let available_width = gutter_dimensions.left_padding - git_gutter_width;
3098
3099 buffer_rows
3100 .iter()
3101 .enumerate()
3102 .map(|(ix, row_info)| {
3103 let ExpandInfo {
3104 excerpt_id,
3105 direction,
3106 } = row_info.expand_info?;
3107
3108 let icon_name = match direction {
3109 ExpandExcerptDirection::Up => IconName::ExpandUp,
3110 ExpandExcerptDirection::Down => IconName::ExpandDown,
3111 ExpandExcerptDirection::UpAndDown => IconName::ExpandVertical,
3112 };
3113
3114 let editor = self.editor.clone();
3115 let is_wide = max_line_number_length
3116 >= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32
3117 && row_info
3118 .buffer_row
3119 .is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)
3120 || gutter_dimensions.right_padding == px(0.);
3121
3122 let width = if is_wide {
3123 available_width - px(2.)
3124 } else {
3125 available_width + em_width - px(2.)
3126 };
3127
3128 let toggle = IconButton::new(("expand", ix), icon_name)
3129 .icon_color(Color::Custom(cx.theme().colors().editor_line_number))
3130 .selected_icon_color(Color::Custom(cx.theme().colors().editor_foreground))
3131 .icon_size(IconSize::Custom(rems(editor_font_size / window.rem_size())))
3132 .width(width)
3133 .on_click(move |_, window, cx| {
3134 editor.update(cx, |editor, cx| {
3135 editor.expand_excerpt(excerpt_id, direction, window, cx);
3136 });
3137 })
3138 .tooltip(Tooltip::for_action_title(
3139 "Expand Excerpt",
3140 &crate::actions::ExpandExcerpts::default(),
3141 ))
3142 .into_any_element();
3143
3144 let position = point(
3145 git_gutter_width + px(1.),
3146 ix as f32 * line_height
3147 - Pixels::from(scroll_top % ScrollPixelOffset::from(line_height))
3148 + px(1.),
3149 );
3150 let origin = gutter_hitbox.origin + position;
3151
3152 Some((toggle, origin))
3153 })
3154 .collect()
3155 }
3156
3157 fn calculate_relative_line_numbers(
3158 &self,
3159 snapshot: &EditorSnapshot,
3160 rows: &Range<DisplayRow>,
3161 relative_to: Option<DisplayRow>,
3162 count_wrapped_lines: bool,
3163 ) -> HashMap<DisplayRow, DisplayRowDelta> {
3164 let mut relative_rows: HashMap<DisplayRow, DisplayRowDelta> = Default::default();
3165 let Some(relative_to) = relative_to else {
3166 return relative_rows;
3167 };
3168
3169 let start = rows.start.min(relative_to);
3170 let end = rows.end.max(relative_to);
3171
3172 let buffer_rows = snapshot
3173 .row_infos(start)
3174 .take(1 + end.minus(start) as usize)
3175 .collect::<Vec<_>>();
3176
3177 let head_idx = relative_to.minus(start);
3178 let mut delta = 1;
3179 let mut i = head_idx + 1;
3180 let should_count_line = |row_info: &RowInfo| {
3181 if count_wrapped_lines {
3182 row_info.buffer_row.is_some() || row_info.wrapped_buffer_row.is_some()
3183 } else {
3184 row_info.buffer_row.is_some()
3185 }
3186 };
3187 while i < buffer_rows.len() as u32 {
3188 if should_count_line(&buffer_rows[i as usize]) {
3189 if rows.contains(&DisplayRow(i + start.0)) {
3190 relative_rows.insert(DisplayRow(i + start.0), delta);
3191 }
3192 delta += 1;
3193 }
3194 i += 1;
3195 }
3196 delta = 1;
3197 i = head_idx.min(buffer_rows.len().saturating_sub(1) as u32);
3198 while i > 0 && buffer_rows[i as usize].buffer_row.is_none() && !count_wrapped_lines {
3199 i -= 1;
3200 }
3201
3202 while i > 0 {
3203 i -= 1;
3204 if should_count_line(&buffer_rows[i as usize]) {
3205 if rows.contains(&DisplayRow(i + start.0)) {
3206 relative_rows.insert(DisplayRow(i + start.0), delta);
3207 }
3208 delta += 1;
3209 }
3210 }
3211
3212 relative_rows
3213 }
3214
3215 fn layout_line_numbers(
3216 &self,
3217 gutter_hitbox: Option<&Hitbox>,
3218 gutter_dimensions: GutterDimensions,
3219 line_height: Pixels,
3220 scroll_position: gpui::Point<ScrollOffset>,
3221 rows: Range<DisplayRow>,
3222 buffer_rows: &[RowInfo],
3223 active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
3224 newest_selection_head: Option<DisplayPoint>,
3225 snapshot: &EditorSnapshot,
3226 window: &mut Window,
3227 cx: &mut App,
3228 ) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> {
3229 let include_line_numbers = snapshot
3230 .show_line_numbers
3231 .unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
3232 if !include_line_numbers {
3233 return Arc::default();
3234 }
3235
3236 let (newest_selection_head, relative) = self.editor.update(cx, |editor, cx| {
3237 let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
3238 let newest = editor
3239 .selections
3240 .newest::<Point>(&editor.display_snapshot(cx));
3241 SelectionLayout::new(
3242 newest,
3243 editor.selections.line_mode(),
3244 editor.cursor_shape,
3245 &snapshot.display_snapshot,
3246 true,
3247 true,
3248 None,
3249 )
3250 .head
3251 });
3252 let relative = editor.relative_line_numbers(cx);
3253 (newest_selection_head, relative)
3254 });
3255
3256 let relative_to = if relative.enabled() {
3257 Some(newest_selection_head.row())
3258 } else {
3259 None
3260 };
3261 let relative_rows =
3262 self.calculate_relative_line_numbers(snapshot, &rows, relative_to, relative.wrapped());
3263 let mut line_number = String::new();
3264 let segments = buffer_rows.iter().enumerate().flat_map(|(ix, row_info)| {
3265 let display_row = DisplayRow(rows.start.0 + ix as u32);
3266 line_number.clear();
3267 let non_relative_number = if relative.wrapped() {
3268 row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1
3269 } else {
3270 row_info.buffer_row? + 1
3271 };
3272 let number = relative_rows
3273 .get(&display_row)
3274 .unwrap_or(&non_relative_number);
3275 write!(&mut line_number, "{number}").unwrap();
3276 if row_info
3277 .diff_status
3278 .is_some_and(|status| status.is_deleted())
3279 {
3280 return None;
3281 }
3282
3283 let color = active_rows
3284 .get(&display_row)
3285 .map(|spec| {
3286 if spec.breakpoint {
3287 cx.theme().colors().debugger_accent
3288 } else {
3289 cx.theme().colors().editor_active_line_number
3290 }
3291 })
3292 .unwrap_or_else(|| cx.theme().colors().editor_line_number);
3293 let shaped_line =
3294 self.shape_line_number(SharedString::from(&line_number), color, window);
3295 let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
3296 let line_origin = gutter_hitbox.map(|hitbox| {
3297 hitbox.origin
3298 + point(
3299 hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
3300 ix as f32 * line_height
3301 - Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)),
3302 )
3303 });
3304
3305 #[cfg(not(test))]
3306 let hitbox = line_origin.map(|line_origin| {
3307 window.insert_hitbox(
3308 Bounds::new(line_origin, size(shaped_line.width, line_height)),
3309 HitboxBehavior::Normal,
3310 )
3311 });
3312 #[cfg(test)]
3313 let hitbox = {
3314 let _ = line_origin;
3315 None
3316 };
3317
3318 let segment = LineNumberSegment {
3319 shaped_line,
3320 hitbox,
3321 };
3322
3323 let buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
3324 let multi_buffer_row = MultiBufferRow(buffer_row);
3325
3326 Some((multi_buffer_row, segment))
3327 });
3328
3329 let mut line_numbers: HashMap<MultiBufferRow, LineNumberLayout> = HashMap::default();
3330 for (buffer_row, segment) in segments {
3331 line_numbers
3332 .entry(buffer_row)
3333 .or_insert_with(|| LineNumberLayout {
3334 segments: Default::default(),
3335 })
3336 .segments
3337 .push(segment);
3338 }
3339 Arc::new(line_numbers)
3340 }
3341
3342 fn layout_crease_toggles(
3343 &self,
3344 rows: Range<DisplayRow>,
3345 row_infos: &[RowInfo],
3346 active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
3347 snapshot: &EditorSnapshot,
3348 window: &mut Window,
3349 cx: &mut App,
3350 ) -> Vec<Option<AnyElement>> {
3351 let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
3352 && snapshot.mode.is_full()
3353 && self.editor.read(cx).buffer_kind(cx) == ItemBufferKind::Singleton;
3354 if include_fold_statuses {
3355 row_infos
3356 .iter()
3357 .enumerate()
3358 .map(|(ix, info)| {
3359 if info.expand_info.is_some() {
3360 return None;
3361 }
3362 let row = info.multibuffer_row?;
3363 let display_row = DisplayRow(rows.start.0 + ix as u32);
3364 let active = active_rows.contains_key(&display_row);
3365
3366 snapshot.render_crease_toggle(row, active, self.editor.clone(), window, cx)
3367 })
3368 .collect()
3369 } else {
3370 Vec::new()
3371 }
3372 }
3373
3374 fn layout_crease_trailers(
3375 &self,
3376 buffer_rows: impl IntoIterator<Item = RowInfo>,
3377 snapshot: &EditorSnapshot,
3378 window: &mut Window,
3379 cx: &mut App,
3380 ) -> Vec<Option<AnyElement>> {
3381 buffer_rows
3382 .into_iter()
3383 .map(|row_info| {
3384 if row_info.expand_info.is_some() {
3385 return None;
3386 }
3387 if let Some(row) = row_info.multibuffer_row {
3388 snapshot.render_crease_trailer(row, window, cx)
3389 } else {
3390 None
3391 }
3392 })
3393 .collect()
3394 }
3395
3396 fn bg_segments_per_row(
3397 rows: Range<DisplayRow>,
3398 selections: &[(PlayerColor, Vec<SelectionLayout>)],
3399 highlight_ranges: &[(Range<DisplayPoint>, Hsla)],
3400 base_background: Hsla,
3401 ) -> Vec<Vec<(Range<DisplayPoint>, Hsla)>> {
3402 if rows.start >= rows.end {
3403 return Vec::new();
3404 }
3405 if !base_background.is_opaque() {
3406 // We don't actually know what color is behind this editor.
3407 return Vec::new();
3408 }
3409 let highlight_iter = highlight_ranges.iter().cloned();
3410 let selection_iter = selections.iter().flat_map(|(player_color, layouts)| {
3411 let color = player_color.selection;
3412 layouts.iter().filter_map(move |selection_layout| {
3413 if selection_layout.range.start != selection_layout.range.end {
3414 Some((selection_layout.range.clone(), color))
3415 } else {
3416 None
3417 }
3418 })
3419 });
3420 let mut per_row_map = vec![Vec::new(); rows.len()];
3421 for (range, color) in highlight_iter.chain(selection_iter) {
3422 let covered_rows = if range.end.column() == 0 {
3423 cmp::max(range.start.row(), rows.start)..cmp::min(range.end.row(), rows.end)
3424 } else {
3425 cmp::max(range.start.row(), rows.start)
3426 ..cmp::min(range.end.row().next_row(), rows.end)
3427 };
3428 for row in covered_rows.iter_rows() {
3429 let seg_start = if row == range.start.row() {
3430 range.start
3431 } else {
3432 DisplayPoint::new(row, 0)
3433 };
3434 let seg_end = if row == range.end.row() && range.end.column() != 0 {
3435 range.end
3436 } else {
3437 DisplayPoint::new(row, u32::MAX)
3438 };
3439 let ix = row.minus(rows.start) as usize;
3440 debug_assert!(row >= rows.start && row < rows.end);
3441 debug_assert!(ix < per_row_map.len());
3442 per_row_map[ix].push((seg_start..seg_end, color));
3443 }
3444 }
3445 for row_segments in per_row_map.iter_mut() {
3446 if row_segments.is_empty() {
3447 continue;
3448 }
3449 let segments = mem::take(row_segments);
3450 let merged = Self::merge_overlapping_ranges(segments, base_background);
3451 *row_segments = merged;
3452 }
3453 per_row_map
3454 }
3455
3456 /// Merge overlapping ranges by splitting at all range boundaries and blending colors where
3457 /// multiple ranges overlap. The result contains non-overlapping ranges ordered from left to right.
3458 ///
3459 /// Expects `start.row() == end.row()` for each range.
3460 fn merge_overlapping_ranges(
3461 ranges: Vec<(Range<DisplayPoint>, Hsla)>,
3462 base_background: Hsla,
3463 ) -> Vec<(Range<DisplayPoint>, Hsla)> {
3464 struct Boundary {
3465 pos: DisplayPoint,
3466 is_start: bool,
3467 index: usize,
3468 color: Hsla,
3469 }
3470
3471 let mut boundaries: SmallVec<[Boundary; 16]> = SmallVec::with_capacity(ranges.len() * 2);
3472 for (index, (range, color)) in ranges.iter().enumerate() {
3473 debug_assert!(
3474 range.start.row() == range.end.row(),
3475 "expects single-row ranges"
3476 );
3477 if range.start < range.end {
3478 boundaries.push(Boundary {
3479 pos: range.start,
3480 is_start: true,
3481 index,
3482 color: *color,
3483 });
3484 boundaries.push(Boundary {
3485 pos: range.end,
3486 is_start: false,
3487 index,
3488 color: *color,
3489 });
3490 }
3491 }
3492
3493 if boundaries.is_empty() {
3494 return Vec::new();
3495 }
3496
3497 boundaries
3498 .sort_unstable_by(|a, b| a.pos.cmp(&b.pos).then_with(|| a.is_start.cmp(&b.is_start)));
3499
3500 let mut processed_ranges: Vec<(Range<DisplayPoint>, Hsla)> = Vec::new();
3501 let mut active_ranges: SmallVec<[(usize, Hsla); 8]> = SmallVec::new();
3502
3503 let mut i = 0;
3504 let mut start_pos = boundaries[0].pos;
3505
3506 let boundaries_len = boundaries.len();
3507 while i < boundaries_len {
3508 let current_boundary_pos = boundaries[i].pos;
3509 if start_pos < current_boundary_pos {
3510 if !active_ranges.is_empty() {
3511 let mut color = base_background;
3512 for &(_, c) in &active_ranges {
3513 color = Hsla::blend(color, c);
3514 }
3515 if let Some((last_range, last_color)) = processed_ranges.last_mut() {
3516 if *last_color == color && last_range.end == start_pos {
3517 last_range.end = current_boundary_pos;
3518 } else {
3519 processed_ranges.push((start_pos..current_boundary_pos, color));
3520 }
3521 } else {
3522 processed_ranges.push((start_pos..current_boundary_pos, color));
3523 }
3524 }
3525 }
3526 while i < boundaries_len && boundaries[i].pos == current_boundary_pos {
3527 let active_range = &boundaries[i];
3528 if active_range.is_start {
3529 let idx = active_range.index;
3530 let pos = active_ranges
3531 .binary_search_by_key(&idx, |(i, _)| *i)
3532 .unwrap_or_else(|p| p);
3533 active_ranges.insert(pos, (idx, active_range.color));
3534 } else {
3535 let idx = active_range.index;
3536 if let Ok(pos) = active_ranges.binary_search_by_key(&idx, |(i, _)| *i) {
3537 active_ranges.remove(pos);
3538 }
3539 }
3540 i += 1;
3541 }
3542 start_pos = current_boundary_pos;
3543 }
3544
3545 processed_ranges
3546 }
3547
3548 fn layout_lines(
3549 rows: Range<DisplayRow>,
3550 snapshot: &EditorSnapshot,
3551 style: &EditorStyle,
3552 editor_width: Pixels,
3553 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
3554 bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
3555 window: &mut Window,
3556 cx: &mut App,
3557 ) -> Vec<LineWithInvisibles> {
3558 if rows.start >= rows.end {
3559 return Vec::new();
3560 }
3561
3562 // Show the placeholder when the editor is empty
3563 if snapshot.is_empty() {
3564 let font_size = style.text.font_size.to_pixels(window.rem_size());
3565 let placeholder_color = cx.theme().colors().text_placeholder;
3566 let placeholder_text = snapshot.placeholder_text();
3567
3568 let placeholder_lines = placeholder_text
3569 .as_ref()
3570 .map_or(Vec::new(), |text| text.split('\n').collect::<Vec<_>>());
3571
3572 let placeholder_line_count = placeholder_lines.len();
3573
3574 placeholder_lines
3575 .into_iter()
3576 .skip(rows.start.0 as usize)
3577 .chain(iter::repeat(""))
3578 .take(cmp::max(rows.len(), placeholder_line_count))
3579 .map(move |line| {
3580 let run = TextRun {
3581 len: line.len(),
3582 font: style.text.font(),
3583 color: placeholder_color,
3584 ..Default::default()
3585 };
3586 let line = window.text_system().shape_line(
3587 line.to_string().into(),
3588 font_size,
3589 &[run],
3590 None,
3591 );
3592 LineWithInvisibles {
3593 width: line.width,
3594 len: line.len,
3595 fragments: smallvec![LineFragment::Text(line)],
3596 invisibles: Vec::new(),
3597 font_size,
3598 }
3599 })
3600 .collect()
3601 } else {
3602 let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
3603 LineWithInvisibles::from_chunks(
3604 chunks,
3605 style,
3606 MAX_LINE_LEN,
3607 rows.len(),
3608 &snapshot.mode,
3609 editor_width,
3610 is_row_soft_wrapped,
3611 bg_segments_per_row,
3612 window,
3613 cx,
3614 )
3615 }
3616 }
3617
3618 fn prepaint_lines(
3619 &self,
3620 start_row: DisplayRow,
3621 line_layouts: &mut [LineWithInvisibles],
3622 line_height: Pixels,
3623 scroll_position: gpui::Point<ScrollOffset>,
3624 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
3625 content_origin: gpui::Point<Pixels>,
3626 window: &mut Window,
3627 cx: &mut App,
3628 ) -> SmallVec<[AnyElement; 1]> {
3629 let mut line_elements = SmallVec::new();
3630 for (ix, line) in line_layouts.iter_mut().enumerate() {
3631 let row = start_row + DisplayRow(ix as u32);
3632 line.prepaint(
3633 line_height,
3634 scroll_position,
3635 scroll_pixel_position,
3636 row,
3637 content_origin,
3638 &mut line_elements,
3639 window,
3640 cx,
3641 );
3642 }
3643 line_elements
3644 }
3645
3646 fn render_block(
3647 &self,
3648 block: &Block,
3649 available_width: AvailableSpace,
3650 block_id: BlockId,
3651 block_row_start: DisplayRow,
3652 snapshot: &EditorSnapshot,
3653 text_x: Pixels,
3654 rows: &Range<DisplayRow>,
3655 line_layouts: &[LineWithInvisibles],
3656 editor_margins: &EditorMargins,
3657 line_height: Pixels,
3658 em_width: Pixels,
3659 text_hitbox: &Hitbox,
3660 editor_width: Pixels,
3661 scroll_width: &mut Pixels,
3662 resized_blocks: &mut HashMap<CustomBlockId, u32>,
3663 row_block_types: &mut HashMap<DisplayRow, bool>,
3664 selections: &[Selection<Point>],
3665 selected_buffer_ids: &Vec<BufferId>,
3666 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
3667 sticky_header_excerpt_id: Option<ExcerptId>,
3668 window: &mut Window,
3669 cx: &mut App,
3670 ) -> Option<(AnyElement, Size<Pixels>, DisplayRow, Pixels)> {
3671 let mut x_position = None;
3672 let mut element = match block {
3673 Block::Custom(custom) => {
3674 let block_start = custom.start().to_point(&snapshot.buffer_snapshot());
3675 let block_end = custom.end().to_point(&snapshot.buffer_snapshot());
3676 if block.place_near() && snapshot.is_line_folded(MultiBufferRow(block_start.row)) {
3677 return None;
3678 }
3679 let align_to = block_start.to_display_point(snapshot);
3680 let x_and_width = |layout: &LineWithInvisibles| {
3681 Some((
3682 text_x + layout.x_for_index(align_to.column() as usize),
3683 text_x + layout.width,
3684 ))
3685 };
3686 let line_ix = align_to.row().0.checked_sub(rows.start.0);
3687 x_position =
3688 if let Some(layout) = line_ix.and_then(|ix| line_layouts.get(ix as usize)) {
3689 x_and_width(layout)
3690 } else {
3691 x_and_width(&layout_line(
3692 align_to.row(),
3693 snapshot,
3694 &self.style,
3695 editor_width,
3696 is_row_soft_wrapped,
3697 window,
3698 cx,
3699 ))
3700 };
3701
3702 let anchor_x = x_position.unwrap().0;
3703
3704 let selected = selections
3705 .binary_search_by(|selection| {
3706 if selection.end <= block_start {
3707 Ordering::Less
3708 } else if selection.start >= block_end {
3709 Ordering::Greater
3710 } else {
3711 Ordering::Equal
3712 }
3713 })
3714 .is_ok();
3715
3716 div()
3717 .size_full()
3718 .child(custom.render(&mut BlockContext {
3719 window,
3720 app: cx,
3721 anchor_x,
3722 margins: editor_margins,
3723 line_height,
3724 em_width,
3725 block_id,
3726 selected,
3727 max_width: text_hitbox.size.width.max(*scroll_width),
3728 editor_style: &self.style,
3729 }))
3730 .into_any()
3731 }
3732
3733 Block::FoldedBuffer {
3734 first_excerpt,
3735 height,
3736 ..
3737 } => {
3738 let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
3739 let result = v_flex().id(block_id).w_full().pr(editor_margins.right);
3740
3741 let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt);
3742 result
3743 .child(self.render_buffer_header(
3744 first_excerpt,
3745 true,
3746 selected,
3747 false,
3748 jump_data,
3749 window,
3750 cx,
3751 ))
3752 .into_any_element()
3753 }
3754
3755 Block::ExcerptBoundary { .. } => {
3756 let color = cx.theme().colors().clone();
3757 let mut result = v_flex().id(block_id).w_full();
3758
3759 result = result.child(
3760 h_flex().relative().child(
3761 div()
3762 .top(line_height / 2.)
3763 .absolute()
3764 .w_full()
3765 .h_px()
3766 .bg(color.border_variant),
3767 ),
3768 );
3769
3770 result.into_any()
3771 }
3772
3773 Block::BufferHeader { excerpt, height } => {
3774 let mut result = v_flex().id(block_id).w_full();
3775
3776 let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt);
3777
3778 if sticky_header_excerpt_id != Some(excerpt.id) {
3779 let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
3780
3781 result = result.child(div().pr(editor_margins.right).child(
3782 self.render_buffer_header(
3783 excerpt, false, selected, false, jump_data, window, cx,
3784 ),
3785 ));
3786 } else {
3787 result =
3788 result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
3789 }
3790
3791 result.into_any()
3792 }
3793 };
3794
3795 // Discover the element's content height, then round up to the nearest multiple of line height.
3796 let preliminary_size = element.layout_as_root(
3797 size(available_width, AvailableSpace::MinContent),
3798 window,
3799 cx,
3800 );
3801 let quantized_height = (preliminary_size.height / line_height).ceil() * line_height;
3802 let final_size = if preliminary_size.height == quantized_height {
3803 preliminary_size
3804 } else {
3805 element.layout_as_root(size(available_width, quantized_height.into()), window, cx)
3806 };
3807 let mut element_height_in_lines = ((final_size.height / line_height).ceil() as u32).max(1);
3808
3809 let mut row = block_row_start;
3810 let mut x_offset = px(0.);
3811 let mut is_block = true;
3812
3813 if let BlockId::Custom(custom_block_id) = block_id
3814 && block.has_height()
3815 {
3816 if block.place_near()
3817 && let Some((x_target, line_width)) = x_position
3818 {
3819 let margin = em_width * 2;
3820 if line_width + final_size.width + margin
3821 < editor_width + editor_margins.gutter.full_width()
3822 && !row_block_types.contains_key(&(row - 1))
3823 && element_height_in_lines == 1
3824 {
3825 x_offset = line_width + margin;
3826 row = row - 1;
3827 is_block = false;
3828 element_height_in_lines = 0;
3829 row_block_types.insert(row, is_block);
3830 } else {
3831 let max_offset =
3832 editor_width + editor_margins.gutter.full_width() - final_size.width;
3833 let min_offset = (x_target + em_width - final_size.width)
3834 .max(editor_margins.gutter.full_width());
3835 x_offset = x_target.min(max_offset).max(min_offset);
3836 }
3837 };
3838 if element_height_in_lines != block.height() {
3839 resized_blocks.insert(custom_block_id, element_height_in_lines);
3840 }
3841 }
3842 for i in 0..element_height_in_lines {
3843 row_block_types.insert(row + i, is_block);
3844 }
3845
3846 Some((element, final_size, row, x_offset))
3847 }
3848
3849 fn render_buffer_header(
3850 &self,
3851 for_excerpt: &ExcerptInfo,
3852 is_folded: bool,
3853 is_selected: bool,
3854 is_sticky: bool,
3855 jump_data: JumpData,
3856 window: &mut Window,
3857 cx: &mut App,
3858 ) -> impl IntoElement {
3859 let editor = self.editor.read(cx);
3860 let multi_buffer = editor.buffer.read(cx);
3861 let file_status = multi_buffer
3862 .all_diff_hunks_expanded()
3863 .then(|| editor.status_for_buffer_id(for_excerpt.buffer_id, cx))
3864 .flatten();
3865 let indicator = multi_buffer
3866 .buffer(for_excerpt.buffer_id)
3867 .and_then(|buffer| {
3868 let buffer = buffer.read(cx);
3869 let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) {
3870 (true, _) => Some(Color::Warning),
3871 (_, true) => Some(Color::Accent),
3872 (false, false) => None,
3873 };
3874 indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color))
3875 });
3876
3877 let include_root = editor
3878 .project
3879 .as_ref()
3880 .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
3881 .unwrap_or_default();
3882 let file = for_excerpt.buffer.file();
3883 let can_open_excerpts = Editor::can_open_excerpts_in_file(file);
3884 let path_style = file.map(|file| file.path_style(cx));
3885 let relative_path = for_excerpt.buffer.resolve_file_path(include_root, cx);
3886 let (parent_path, filename) = if let Some(path) = &relative_path {
3887 if let Some(path_style) = path_style {
3888 let (dir, file_name) = path_style.split(path);
3889 (dir.map(|dir| dir.to_owned()), Some(file_name.to_owned()))
3890 } else {
3891 (None, Some(path.clone()))
3892 }
3893 } else {
3894 (None, None)
3895 };
3896 let focus_handle = editor.focus_handle(cx);
3897 let colors = cx.theme().colors();
3898
3899 let header = div()
3900 .p_1()
3901 .w_full()
3902 .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
3903 .child(
3904 h_flex()
3905 .size_full()
3906 .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
3907 .pl_1()
3908 .pr_2()
3909 .rounded_sm()
3910 .gap_1p5()
3911 .when(is_sticky, |el| el.shadow_md())
3912 .border_1()
3913 .map(|div| {
3914 let border_color = if is_selected
3915 && is_folded
3916 && focus_handle.contains_focused(window, cx)
3917 {
3918 colors.border_focused
3919 } else {
3920 colors.border
3921 };
3922 div.border_color(border_color)
3923 })
3924 .bg(colors.editor_subheader_background)
3925 .hover(|style| style.bg(colors.element_hover))
3926 .map(|header| {
3927 let editor = self.editor.clone();
3928 let buffer_id = for_excerpt.buffer_id;
3929 let toggle_chevron_icon =
3930 FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
3931 let button_size = rems_from_px(28.);
3932
3933 header.child(
3934 div()
3935 .hover(|style| style.bg(colors.element_selected))
3936 .rounded_xs()
3937 .child(
3938 ButtonLike::new("toggle-buffer-fold")
3939 .style(ButtonStyle::Transparent)
3940 .height(button_size.into())
3941 .width(button_size)
3942 .children(toggle_chevron_icon)
3943 .tooltip({
3944 let focus_handle = focus_handle.clone();
3945 move |_window, cx| {
3946 Tooltip::with_meta_in(
3947 "Toggle Excerpt Fold",
3948 Some(&ToggleFold),
3949 format!(
3950 "{} to toggle all",
3951 text_for_keystroke(
3952 &Modifiers::alt(),
3953 "click",
3954 cx
3955 )
3956 ),
3957 &focus_handle,
3958 cx,
3959 )
3960 }
3961 })
3962 .on_click(move |event, window, cx| {
3963 if event.modifiers().alt {
3964 // Alt+click toggles all buffers
3965 editor.update(cx, |editor, cx| {
3966 editor.toggle_fold_all(
3967 &ToggleFoldAll,
3968 window,
3969 cx,
3970 );
3971 });
3972 } else {
3973 // Regular click toggles single buffer
3974 if is_folded {
3975 editor.update(cx, |editor, cx| {
3976 editor.unfold_buffer(buffer_id, cx);
3977 });
3978 } else {
3979 editor.update(cx, |editor, cx| {
3980 editor.fold_buffer(buffer_id, cx);
3981 });
3982 }
3983 }
3984 }),
3985 ),
3986 )
3987 })
3988 .children(
3989 editor
3990 .addons
3991 .values()
3992 .filter_map(|addon| {
3993 addon.render_buffer_header_controls(for_excerpt, window, cx)
3994 })
3995 .take(1),
3996 )
3997 .child(
3998 h_flex()
3999 .size(rems_from_px(12.0))
4000 .justify_center()
4001 .flex_shrink_0()
4002 .children(indicator),
4003 )
4004 .child(
4005 h_flex()
4006 .cursor_pointer()
4007 .id("path header block")
4008 .size_full()
4009 .justify_between()
4010 .overflow_hidden()
4011 .child(h_flex().gap_0p5().map(|path_header| {
4012 let filename = filename
4013 .map(SharedString::from)
4014 .unwrap_or_else(|| "untitled".into());
4015
4016 path_header
4017 .when(ItemSettings::get_global(cx).file_icons, |el| {
4018 let path = path::Path::new(filename.as_str());
4019 let icon =
4020 FileIcons::get_icon(path, cx).unwrap_or_default();
4021 let icon = Icon::from_path(icon).color(Color::Muted);
4022 el.child(icon)
4023 })
4024 .child(
4025 ButtonLike::new("filename-button")
4026 .style(ButtonStyle::Subtle)
4027 .child(
4028 div()
4029 .child(
4030 Label::new(filename)
4031 .single_line()
4032 .color(file_status_label_color(
4033 file_status,
4034 ))
4035 .when(
4036 file_status.is_some_and(|s| {
4037 s.is_deleted()
4038 }),
4039 |label| label.strikethrough(),
4040 ),
4041 )
4042 .group_hover("", |div| div.underline()),
4043 )
4044 .on_click(window.listener_for(&self.editor, {
4045 let jump_data = jump_data.clone();
4046 move |editor, e: &ClickEvent, window, cx| {
4047 editor.open_excerpts_common(
4048 Some(jump_data.clone()),
4049 e.modifiers().secondary(),
4050 window,
4051 cx,
4052 );
4053 }
4054 })),
4055 )
4056 .when_some(parent_path, |then, path| {
4057 then.child(div().child(path).text_color(
4058 if file_status.is_some_and(FileStatus::is_deleted) {
4059 colors.text_disabled
4060 } else {
4061 colors.text_muted
4062 },
4063 ))
4064 })
4065 }))
4066 .when(
4067 can_open_excerpts && is_selected && relative_path.is_some(),
4068 |el| {
4069 el.child(
4070 Button::new("open-file", "Open File")
4071 .style(ButtonStyle::OutlinedGhost)
4072 .key_binding(KeyBinding::for_action_in(
4073 &OpenExcerpts,
4074 &focus_handle,
4075 cx,
4076 ))
4077 .on_click(window.listener_for(&self.editor, {
4078 let jump_data = jump_data.clone();
4079 move |editor, e: &ClickEvent, window, cx| {
4080 editor.open_excerpts_common(
4081 Some(jump_data.clone()),
4082 e.modifiers().secondary(),
4083 window,
4084 cx,
4085 );
4086 }
4087 })),
4088 )
4089 },
4090 )
4091 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
4092 .on_click(window.listener_for(&self.editor, {
4093 let buffer_id = for_excerpt.buffer_id;
4094 move |editor, _e: &ClickEvent, _window, cx| {
4095 if is_folded {
4096 editor.unfold_buffer(buffer_id, cx);
4097 } else {
4098 editor.fold_buffer(buffer_id, cx);
4099 }
4100 }
4101 })),
4102 ),
4103 );
4104
4105 let file = for_excerpt.buffer.file().cloned();
4106 let editor = self.editor.clone();
4107
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 ..Default::default()
7444 }],
7445 None,
7446 );
7447
7448 layout.width
7449 }
7450
7451 fn max_line_number_width(&self, snapshot: &EditorSnapshot, window: &mut Window) -> Pixels {
7452 let digit_count = snapshot.widest_line_number().ilog10() + 1;
7453 self.column_pixels(digit_count as usize, window)
7454 }
7455
7456 fn shape_line_number(
7457 &self,
7458 text: SharedString,
7459 color: Hsla,
7460 window: &mut Window,
7461 ) -> ShapedLine {
7462 let run = TextRun {
7463 len: text.len(),
7464 font: self.style.text.font(),
7465 color,
7466 ..Default::default()
7467 };
7468 window.text_system().shape_line(
7469 text,
7470 self.style.text.font_size.to_pixels(window.rem_size()),
7471 &[run],
7472 None,
7473 )
7474 }
7475
7476 fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
7477 let unstaged = status.has_secondary_hunk();
7478 let unstaged_hollow = matches!(
7479 ProjectSettings::get_global(cx).git.hunk_style,
7480 GitHunkStyleSetting::UnstagedHollow
7481 );
7482
7483 unstaged == unstaged_hollow
7484 }
7485
7486 #[cfg(debug_assertions)]
7487 fn layout_debug_ranges(
7488 selections: &mut Vec<(PlayerColor, Vec<SelectionLayout>)>,
7489 anchor_range: Range<Anchor>,
7490 display_snapshot: &DisplaySnapshot,
7491 cx: &App,
7492 ) {
7493 let theme = cx.theme();
7494 text::debug::GlobalDebugRanges::with_locked(|debug_ranges| {
7495 if debug_ranges.ranges.is_empty() {
7496 return;
7497 }
7498 let buffer_snapshot = &display_snapshot.buffer_snapshot();
7499 for (buffer, buffer_range, excerpt_id) in
7500 buffer_snapshot.range_to_buffer_ranges(anchor_range)
7501 {
7502 let buffer_range =
7503 buffer.anchor_after(buffer_range.start)..buffer.anchor_before(buffer_range.end);
7504 selections.extend(debug_ranges.ranges.iter().flat_map(|debug_range| {
7505 let player_color = theme
7506 .players()
7507 .color_for_participant(debug_range.occurrence_index as u32 + 1);
7508 debug_range.ranges.iter().filter_map(move |range| {
7509 if range.start.buffer_id != Some(buffer.remote_id()) {
7510 return None;
7511 }
7512 let clipped_start = range.start.max(&buffer_range.start, buffer);
7513 let clipped_end = range.end.min(&buffer_range.end, buffer);
7514 let range = buffer_snapshot
7515 .anchor_range_in_excerpt(excerpt_id, *clipped_start..*clipped_end)?;
7516 let start = range.start.to_display_point(display_snapshot);
7517 let end = range.end.to_display_point(display_snapshot);
7518 let selection_layout = SelectionLayout {
7519 head: start,
7520 range: start..end,
7521 cursor_shape: CursorShape::Bar,
7522 is_newest: false,
7523 is_local: false,
7524 active_rows: start.row()..end.row(),
7525 user_name: Some(SharedString::new(debug_range.value.clone())),
7526 };
7527 Some((player_color, vec![selection_layout]))
7528 })
7529 }));
7530 }
7531 });
7532 }
7533}
7534
7535fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
7536 file_status.map_or(Color::Default, |status| {
7537 if status.is_conflicted() {
7538 Color::Conflict
7539 } else if status.is_modified() {
7540 Color::Modified
7541 } else if status.is_deleted() {
7542 Color::Disabled
7543 } else if status.is_created() {
7544 Color::Created
7545 } else {
7546 Color::Default
7547 }
7548 })
7549}
7550
7551fn header_jump_data(
7552 snapshot: &EditorSnapshot,
7553 block_row_start: DisplayRow,
7554 height: u32,
7555 for_excerpt: &ExcerptInfo,
7556) -> JumpData {
7557 let range = &for_excerpt.range;
7558 let buffer = &for_excerpt.buffer;
7559 let jump_anchor = range.primary.start;
7560
7561 let excerpt_start = range.context.start;
7562 let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
7563 let rows_from_excerpt_start = if jump_anchor == excerpt_start {
7564 0
7565 } else {
7566 let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
7567 jump_position.row.saturating_sub(excerpt_start_point.row)
7568 };
7569
7570 let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start)
7571 .saturating_sub(
7572 snapshot
7573 .scroll_anchor
7574 .scroll_position(&snapshot.display_snapshot)
7575 .y as u32,
7576 );
7577
7578 JumpData::MultiBufferPoint {
7579 excerpt_id: for_excerpt.id,
7580 anchor: jump_anchor,
7581 position: jump_position,
7582 line_offset_from_top,
7583 }
7584}
7585
7586pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
7587
7588impl AcceptEditPredictionBinding {
7589 pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
7590 if let Some(binding) = self.0.as_ref() {
7591 match &binding.keystrokes() {
7592 [keystroke, ..] => Some(keystroke),
7593 _ => None,
7594 }
7595 } else {
7596 None
7597 }
7598 }
7599}
7600
7601fn prepaint_gutter_button(
7602 button: IconButton,
7603 row: DisplayRow,
7604 line_height: Pixels,
7605 gutter_dimensions: &GutterDimensions,
7606 scroll_position: gpui::Point<ScrollOffset>,
7607 gutter_hitbox: &Hitbox,
7608 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
7609 window: &mut Window,
7610 cx: &mut App,
7611) -> AnyElement {
7612 let mut button = button.into_any_element();
7613
7614 let available_space = size(
7615 AvailableSpace::MinContent,
7616 AvailableSpace::Definite(line_height),
7617 );
7618 let indicator_size = button.layout_as_root(available_space, window, cx);
7619
7620 let blame_width = gutter_dimensions.git_blame_entries_width;
7621 let gutter_width = display_hunks
7622 .binary_search_by(|(hunk, _)| match hunk {
7623 DisplayDiffHunk::Folded { display_row } => display_row.cmp(&row),
7624 DisplayDiffHunk::Unfolded {
7625 display_row_range, ..
7626 } => {
7627 if display_row_range.end <= row {
7628 Ordering::Less
7629 } else if display_row_range.start > row {
7630 Ordering::Greater
7631 } else {
7632 Ordering::Equal
7633 }
7634 }
7635 })
7636 .ok()
7637 .and_then(|ix| Some(display_hunks[ix].1.as_ref()?.size.width));
7638 let left_offset = blame_width.max(gutter_width).unwrap_or_default();
7639
7640 let mut x = left_offset;
7641 let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
7642 - indicator_size.width
7643 - left_offset;
7644 x += available_width / 2.;
7645
7646 let mut y =
7647 Pixels::from((row.as_f64() - scroll_position.y) * ScrollPixelOffset::from(line_height));
7648 y += (line_height - indicator_size.height) / 2.;
7649
7650 button.prepaint_as_root(
7651 gutter_hitbox.origin + point(x, y),
7652 available_space,
7653 window,
7654 cx,
7655 );
7656 button
7657}
7658
7659fn render_inline_blame_entry(
7660 blame_entry: BlameEntry,
7661 style: &EditorStyle,
7662 cx: &mut App,
7663) -> Option<AnyElement> {
7664 let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
7665 renderer.render_inline_blame_entry(&style.text, blame_entry, cx)
7666}
7667
7668fn render_blame_entry_popover(
7669 blame_entry: BlameEntry,
7670 scroll_handle: ScrollHandle,
7671 commit_message: Option<ParsedCommitMessage>,
7672 markdown: Entity<Markdown>,
7673 workspace: WeakEntity<Workspace>,
7674 blame: &Entity<GitBlame>,
7675 buffer: BufferId,
7676 window: &mut Window,
7677 cx: &mut App,
7678) -> Option<AnyElement> {
7679 let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
7680 let blame = blame.read(cx);
7681 let repository = blame.repository(cx, buffer)?;
7682 renderer.render_blame_entry_popover(
7683 blame_entry,
7684 scroll_handle,
7685 commit_message,
7686 markdown,
7687 repository,
7688 workspace,
7689 window,
7690 cx,
7691 )
7692}
7693
7694fn render_blame_entry(
7695 ix: usize,
7696 blame: &Entity<GitBlame>,
7697 blame_entry: BlameEntry,
7698 style: &EditorStyle,
7699 last_used_color: &mut Option<(Hsla, Oid)>,
7700 editor: Entity<Editor>,
7701 workspace: Entity<Workspace>,
7702 buffer: BufferId,
7703 renderer: &dyn BlameRenderer,
7704 window: &mut Window,
7705 cx: &mut App,
7706) -> Option<AnyElement> {
7707 let index: u32 = blame_entry.sha.into();
7708 let mut sha_color = cx.theme().players().color_for_participant(index).cursor;
7709
7710 // If the last color we used is the same as the one we get for this line, but
7711 // the commit SHAs are different, then we try again to get a different color.
7712 if let Some((color, sha)) = *last_used_color
7713 && sha != blame_entry.sha
7714 && color == sha_color
7715 {
7716 sha_color = cx.theme().players().color_for_participant(index + 1).cursor;
7717 }
7718 last_used_color.replace((sha_color, blame_entry.sha));
7719
7720 let blame = blame.read(cx);
7721 let details = blame.details_for_entry(buffer, &blame_entry);
7722 let repository = blame.repository(cx, buffer)?;
7723 renderer.render_blame_entry(
7724 &style.text,
7725 blame_entry,
7726 details,
7727 repository,
7728 workspace.downgrade(),
7729 editor,
7730 ix,
7731 sha_color,
7732 window,
7733 cx,
7734 )
7735}
7736
7737#[derive(Debug)]
7738pub(crate) struct LineWithInvisibles {
7739 fragments: SmallVec<[LineFragment; 1]>,
7740 invisibles: Vec<Invisible>,
7741 len: usize,
7742 pub(crate) width: Pixels,
7743 font_size: Pixels,
7744}
7745
7746enum LineFragment {
7747 Text(ShapedLine),
7748 Element {
7749 id: ChunkRendererId,
7750 element: Option<AnyElement>,
7751 size: Size<Pixels>,
7752 len: usize,
7753 },
7754}
7755
7756impl fmt::Debug for LineFragment {
7757 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
7758 match self {
7759 LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
7760 LineFragment::Element { size, len, .. } => f
7761 .debug_struct("Element")
7762 .field("size", size)
7763 .field("len", len)
7764 .finish(),
7765 }
7766 }
7767}
7768
7769impl LineWithInvisibles {
7770 fn from_chunks<'a>(
7771 chunks: impl Iterator<Item = HighlightedChunk<'a>>,
7772 editor_style: &EditorStyle,
7773 max_line_len: usize,
7774 max_line_count: usize,
7775 editor_mode: &EditorMode,
7776 text_width: Pixels,
7777 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
7778 bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
7779 window: &mut Window,
7780 cx: &mut App,
7781 ) -> Vec<Self> {
7782 let text_style = &editor_style.text;
7783 let mut layouts = Vec::with_capacity(max_line_count);
7784 let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
7785 let mut line = String::new();
7786 let mut invisibles = Vec::new();
7787 let mut width = Pixels::ZERO;
7788 let mut len = 0;
7789 let mut styles = Vec::new();
7790 let mut non_whitespace_added = false;
7791 let mut row = 0;
7792 let mut line_exceeded_max_len = false;
7793 let font_size = text_style.font_size.to_pixels(window.rem_size());
7794 let min_contrast = EditorSettings::get_global(cx).minimum_contrast_for_highlights;
7795
7796 let ellipsis = SharedString::from("β―");
7797
7798 for highlighted_chunk in chunks.chain([HighlightedChunk {
7799 text: "\n",
7800 style: None,
7801 is_tab: false,
7802 is_inlay: false,
7803 replacement: None,
7804 }]) {
7805 if let Some(replacement) = highlighted_chunk.replacement {
7806 if !line.is_empty() {
7807 let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
7808 let text_runs: &[TextRun] = if segments.is_empty() {
7809 &styles
7810 } else {
7811 &Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
7812 };
7813 let shaped_line = window.text_system().shape_line(
7814 line.clone().into(),
7815 font_size,
7816 text_runs,
7817 None,
7818 );
7819 width += shaped_line.width;
7820 len += shaped_line.len;
7821 fragments.push(LineFragment::Text(shaped_line));
7822 line.clear();
7823 styles.clear();
7824 }
7825
7826 match replacement {
7827 ChunkReplacement::Renderer(renderer) => {
7828 let available_width = if renderer.constrain_width {
7829 let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
7830 ellipsis.clone()
7831 } else {
7832 SharedString::from(Arc::from(highlighted_chunk.text))
7833 };
7834 let shaped_line = window.text_system().shape_line(
7835 chunk,
7836 font_size,
7837 &[text_style.to_run(highlighted_chunk.text.len())],
7838 None,
7839 );
7840 AvailableSpace::Definite(shaped_line.width)
7841 } else {
7842 AvailableSpace::MinContent
7843 };
7844
7845 let mut element = (renderer.render)(&mut ChunkRendererContext {
7846 context: cx,
7847 window,
7848 max_width: text_width,
7849 });
7850 let line_height = text_style.line_height_in_pixels(window.rem_size());
7851 let size = element.layout_as_root(
7852 size(available_width, AvailableSpace::Definite(line_height)),
7853 window,
7854 cx,
7855 );
7856
7857 width += size.width;
7858 len += highlighted_chunk.text.len();
7859 fragments.push(LineFragment::Element {
7860 id: renderer.id,
7861 element: Some(element),
7862 size,
7863 len: highlighted_chunk.text.len(),
7864 });
7865 }
7866 ChunkReplacement::Str(x) => {
7867 let text_style = if let Some(style) = highlighted_chunk.style {
7868 Cow::Owned(text_style.clone().highlight(style))
7869 } else {
7870 Cow::Borrowed(text_style)
7871 };
7872
7873 let run = TextRun {
7874 len: x.len(),
7875 font: text_style.font(),
7876 color: text_style.color,
7877 background_color: text_style.background_color,
7878 underline: text_style.underline,
7879 strikethrough: text_style.strikethrough,
7880 };
7881 let line_layout = window
7882 .text_system()
7883 .shape_line(x, font_size, &[run], None)
7884 .with_len(highlighted_chunk.text.len());
7885
7886 width += line_layout.width;
7887 len += highlighted_chunk.text.len();
7888 fragments.push(LineFragment::Text(line_layout))
7889 }
7890 }
7891 } else {
7892 for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
7893 if ix > 0 {
7894 let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
7895 let text_runs = if segments.is_empty() {
7896 &styles
7897 } else {
7898 &Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
7899 };
7900 let shaped_line = window.text_system().shape_line(
7901 line.clone().into(),
7902 font_size,
7903 text_runs,
7904 None,
7905 );
7906 width += shaped_line.width;
7907 len += shaped_line.len;
7908 fragments.push(LineFragment::Text(shaped_line));
7909 layouts.push(Self {
7910 width: mem::take(&mut width),
7911 len: mem::take(&mut len),
7912 fragments: mem::take(&mut fragments),
7913 invisibles: std::mem::take(&mut invisibles),
7914 font_size,
7915 });
7916
7917 line.clear();
7918 styles.clear();
7919 row += 1;
7920 line_exceeded_max_len = false;
7921 non_whitespace_added = false;
7922 if row == max_line_count {
7923 return layouts;
7924 }
7925 }
7926
7927 if !line_chunk.is_empty() && !line_exceeded_max_len {
7928 let text_style = if let Some(style) = highlighted_chunk.style {
7929 Cow::Owned(text_style.clone().highlight(style))
7930 } else {
7931 Cow::Borrowed(text_style)
7932 };
7933
7934 if line.len() + line_chunk.len() > max_line_len {
7935 let mut chunk_len = max_line_len - line.len();
7936 while !line_chunk.is_char_boundary(chunk_len) {
7937 chunk_len -= 1;
7938 }
7939 line_chunk = &line_chunk[..chunk_len];
7940 line_exceeded_max_len = true;
7941 }
7942
7943 styles.push(TextRun {
7944 len: line_chunk.len(),
7945 font: text_style.font(),
7946 color: text_style.color,
7947 background_color: text_style.background_color,
7948 underline: text_style.underline,
7949 strikethrough: text_style.strikethrough,
7950 });
7951
7952 if editor_mode.is_full() && !highlighted_chunk.is_inlay {
7953 // Line wrap pads its contents with fake whitespaces,
7954 // avoid printing them
7955 let is_soft_wrapped = is_row_soft_wrapped(row);
7956 if highlighted_chunk.is_tab {
7957 if non_whitespace_added || !is_soft_wrapped {
7958 invisibles.push(Invisible::Tab {
7959 line_start_offset: line.len(),
7960 line_end_offset: line.len() + line_chunk.len(),
7961 });
7962 }
7963 } else {
7964 invisibles.extend(line_chunk.char_indices().filter_map(
7965 |(index, c)| {
7966 let is_whitespace = c.is_whitespace();
7967 non_whitespace_added |= !is_whitespace;
7968 if is_whitespace
7969 && (non_whitespace_added || !is_soft_wrapped)
7970 {
7971 Some(Invisible::Whitespace {
7972 line_offset: line.len() + index,
7973 })
7974 } else {
7975 None
7976 }
7977 },
7978 ))
7979 }
7980 }
7981
7982 line.push_str(line_chunk);
7983 }
7984 }
7985 }
7986 }
7987
7988 layouts
7989 }
7990
7991 /// Takes text runs and non-overlapping left-to-right background ranges with color.
7992 /// Returns new text runs with adjusted contrast as per background ranges.
7993 fn split_runs_by_bg_segments(
7994 text_runs: &[TextRun],
7995 bg_segments: &[(Range<DisplayPoint>, Hsla)],
7996 min_contrast: f32,
7997 start_col_offset: usize,
7998 ) -> Vec<TextRun> {
7999 let mut output_runs: Vec<TextRun> = Vec::with_capacity(text_runs.len());
8000 let mut line_col = start_col_offset;
8001 let mut segment_ix = 0usize;
8002
8003 for text_run in text_runs.iter() {
8004 let run_start_col = line_col;
8005 let run_end_col = run_start_col + text_run.len;
8006 while segment_ix < bg_segments.len()
8007 && (bg_segments[segment_ix].0.end.column() as usize) <= run_start_col
8008 {
8009 segment_ix += 1;
8010 }
8011 let mut cursor_col = run_start_col;
8012 let mut local_segment_ix = segment_ix;
8013 while local_segment_ix < bg_segments.len() {
8014 let (range, segment_color) = &bg_segments[local_segment_ix];
8015 let segment_start_col = range.start.column() as usize;
8016 let segment_end_col = range.end.column() as usize;
8017 if segment_start_col >= run_end_col {
8018 break;
8019 }
8020 if segment_start_col > cursor_col {
8021 let span_len = segment_start_col - cursor_col;
8022 output_runs.push(TextRun {
8023 len: span_len,
8024 font: text_run.font.clone(),
8025 color: text_run.color,
8026 background_color: text_run.background_color,
8027 underline: text_run.underline,
8028 strikethrough: text_run.strikethrough,
8029 });
8030 cursor_col = segment_start_col;
8031 }
8032 let segment_slice_end_col = segment_end_col.min(run_end_col);
8033 if segment_slice_end_col > cursor_col {
8034 let new_text_color =
8035 ensure_minimum_contrast(text_run.color, *segment_color, min_contrast);
8036 output_runs.push(TextRun {
8037 len: segment_slice_end_col - cursor_col,
8038 font: text_run.font.clone(),
8039 color: new_text_color,
8040 background_color: text_run.background_color,
8041 underline: text_run.underline,
8042 strikethrough: text_run.strikethrough,
8043 });
8044 cursor_col = segment_slice_end_col;
8045 }
8046 if segment_end_col >= run_end_col {
8047 break;
8048 }
8049 local_segment_ix += 1;
8050 }
8051 if cursor_col < run_end_col {
8052 output_runs.push(TextRun {
8053 len: run_end_col - cursor_col,
8054 font: text_run.font.clone(),
8055 color: text_run.color,
8056 background_color: text_run.background_color,
8057 underline: text_run.underline,
8058 strikethrough: text_run.strikethrough,
8059 });
8060 }
8061 line_col = run_end_col;
8062 segment_ix = local_segment_ix;
8063 }
8064 output_runs
8065 }
8066
8067 fn prepaint(
8068 &mut self,
8069 line_height: Pixels,
8070 scroll_position: gpui::Point<ScrollOffset>,
8071 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
8072 row: DisplayRow,
8073 content_origin: gpui::Point<Pixels>,
8074 line_elements: &mut SmallVec<[AnyElement; 1]>,
8075 window: &mut Window,
8076 cx: &mut App,
8077 ) {
8078 let line_y = f32::from(line_height) * Pixels::from(row.as_f64() - scroll_position.y);
8079 let mut fragment_origin =
8080 content_origin + gpui::point(Pixels::from(-scroll_pixel_position.x), line_y);
8081 for fragment in &mut self.fragments {
8082 match fragment {
8083 LineFragment::Text(line) => {
8084 fragment_origin.x += line.width;
8085 }
8086 LineFragment::Element { element, size, .. } => {
8087 let mut element = element
8088 .take()
8089 .expect("you can't prepaint LineWithInvisibles twice");
8090
8091 // Center the element vertically within the line.
8092 let mut element_origin = fragment_origin;
8093 element_origin.y += (line_height - size.height) / 2.;
8094 element.prepaint_at(element_origin, window, cx);
8095 line_elements.push(element);
8096
8097 fragment_origin.x += size.width;
8098 }
8099 }
8100 }
8101 }
8102
8103 fn draw(
8104 &self,
8105 layout: &EditorLayout,
8106 row: DisplayRow,
8107 content_origin: gpui::Point<Pixels>,
8108 whitespace_setting: ShowWhitespaceSetting,
8109 selection_ranges: &[Range<DisplayPoint>],
8110 window: &mut Window,
8111 cx: &mut App,
8112 ) {
8113 let line_height = layout.position_map.line_height;
8114 let line_y = line_height * (row.as_f64() - layout.position_map.scroll_position.y) as f32;
8115
8116 let mut fragment_origin = content_origin
8117 + gpui::point(
8118 Pixels::from(-layout.position_map.scroll_pixel_position.x),
8119 line_y,
8120 );
8121
8122 for fragment in &self.fragments {
8123 match fragment {
8124 LineFragment::Text(line) => {
8125 line.paint(fragment_origin, line_height, window, cx)
8126 .log_err();
8127 fragment_origin.x += line.width;
8128 }
8129 LineFragment::Element { size, .. } => {
8130 fragment_origin.x += size.width;
8131 }
8132 }
8133 }
8134
8135 self.draw_invisibles(
8136 selection_ranges,
8137 layout,
8138 content_origin,
8139 line_y,
8140 row,
8141 line_height,
8142 whitespace_setting,
8143 window,
8144 cx,
8145 );
8146 }
8147
8148 fn draw_background(
8149 &self,
8150 layout: &EditorLayout,
8151 row: DisplayRow,
8152 content_origin: gpui::Point<Pixels>,
8153 window: &mut Window,
8154 cx: &mut App,
8155 ) {
8156 let line_height = layout.position_map.line_height;
8157 let line_y = line_height * (row.as_f64() - layout.position_map.scroll_position.y) as f32;
8158
8159 let mut fragment_origin = content_origin
8160 + gpui::point(
8161 Pixels::from(-layout.position_map.scroll_pixel_position.x),
8162 line_y,
8163 );
8164
8165 for fragment in &self.fragments {
8166 match fragment {
8167 LineFragment::Text(line) => {
8168 line.paint_background(fragment_origin, line_height, window, cx)
8169 .log_err();
8170 fragment_origin.x += line.width;
8171 }
8172 LineFragment::Element { size, .. } => {
8173 fragment_origin.x += size.width;
8174 }
8175 }
8176 }
8177 }
8178
8179 fn draw_invisibles(
8180 &self,
8181 selection_ranges: &[Range<DisplayPoint>],
8182 layout: &EditorLayout,
8183 content_origin: gpui::Point<Pixels>,
8184 line_y: Pixels,
8185 row: DisplayRow,
8186 line_height: Pixels,
8187 whitespace_setting: ShowWhitespaceSetting,
8188 window: &mut Window,
8189 cx: &mut App,
8190 ) {
8191 let extract_whitespace_info = |invisible: &Invisible| {
8192 let (token_offset, token_end_offset, invisible_symbol) = match invisible {
8193 Invisible::Tab {
8194 line_start_offset,
8195 line_end_offset,
8196 } => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
8197 Invisible::Whitespace { line_offset } => {
8198 (*line_offset, line_offset + 1, &layout.space_invisible)
8199 }
8200 };
8201
8202 let x_offset: ScrollPixelOffset = self.x_for_index(token_offset).into();
8203 let invisible_offset: ScrollPixelOffset =
8204 ((layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0)
8205 .into();
8206 let origin = content_origin
8207 + gpui::point(
8208 Pixels::from(
8209 x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x,
8210 ),
8211 line_y,
8212 );
8213
8214 (
8215 [token_offset, token_end_offset],
8216 Box::new(move |window: &mut Window, cx: &mut App| {
8217 invisible_symbol
8218 .paint(origin, line_height, window, cx)
8219 .log_err();
8220 }),
8221 )
8222 };
8223
8224 let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
8225 match whitespace_setting {
8226 ShowWhitespaceSetting::None => (),
8227 ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(window, cx)),
8228 ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
8229 let invisible_point = DisplayPoint::new(row, start as u32);
8230 if !selection_ranges
8231 .iter()
8232 .any(|region| region.start <= invisible_point && invisible_point < region.end)
8233 {
8234 return;
8235 }
8236
8237 paint(window, cx);
8238 }),
8239
8240 ShowWhitespaceSetting::Trailing => {
8241 let mut previous_start = self.len;
8242 for ([start, end], paint) in invisible_iter.rev() {
8243 if previous_start != end {
8244 break;
8245 }
8246 previous_start = start;
8247 paint(window, cx);
8248 }
8249 }
8250
8251 // For a whitespace to be on a boundary, any of the following conditions need to be met:
8252 // - It is a tab
8253 // - It is adjacent to an edge (start or end)
8254 // - It is adjacent to a whitespace (left or right)
8255 ShowWhitespaceSetting::Boundary => {
8256 // 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
8257 // the above cases.
8258 // Note: We zip in the original `invisibles` to check for tab equality
8259 let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut Window, &mut App)>)> = None;
8260 for (([start, end], paint), invisible) in
8261 invisible_iter.zip_eq(self.invisibles.iter())
8262 {
8263 let should_render = match (&last_seen, invisible) {
8264 (_, Invisible::Tab { .. }) => true,
8265 (Some((_, last_end, _)), _) => *last_end == start,
8266 _ => false,
8267 };
8268
8269 if should_render || start == 0 || end == self.len {
8270 paint(window, cx);
8271
8272 // Since we are scanning from the left, we will skip over the first available whitespace that is part
8273 // of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
8274 if let Some((should_render_last, last_end, paint_last)) = last_seen {
8275 // Note that we need to make sure that the last one is actually adjacent
8276 if !should_render_last && last_end == start {
8277 paint_last(window, cx);
8278 }
8279 }
8280 }
8281
8282 // Manually render anything within a selection
8283 let invisible_point = DisplayPoint::new(row, start as u32);
8284 if selection_ranges.iter().any(|region| {
8285 region.start <= invisible_point && invisible_point < region.end
8286 }) {
8287 paint(window, cx);
8288 }
8289
8290 last_seen = Some((should_render, end, paint));
8291 }
8292 }
8293 }
8294 }
8295
8296 pub fn x_for_index(&self, index: usize) -> Pixels {
8297 let mut fragment_start_x = Pixels::ZERO;
8298 let mut fragment_start_index = 0;
8299
8300 for fragment in &self.fragments {
8301 match fragment {
8302 LineFragment::Text(shaped_line) => {
8303 let fragment_end_index = fragment_start_index + shaped_line.len;
8304 if index < fragment_end_index {
8305 return fragment_start_x
8306 + shaped_line.x_for_index(index - fragment_start_index);
8307 }
8308 fragment_start_x += shaped_line.width;
8309 fragment_start_index = fragment_end_index;
8310 }
8311 LineFragment::Element { len, size, .. } => {
8312 let fragment_end_index = fragment_start_index + len;
8313 if index < fragment_end_index {
8314 return fragment_start_x;
8315 }
8316 fragment_start_x += size.width;
8317 fragment_start_index = fragment_end_index;
8318 }
8319 }
8320 }
8321
8322 fragment_start_x
8323 }
8324
8325 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
8326 let mut fragment_start_x = Pixels::ZERO;
8327 let mut fragment_start_index = 0;
8328
8329 for fragment in &self.fragments {
8330 match fragment {
8331 LineFragment::Text(shaped_line) => {
8332 let fragment_end_x = fragment_start_x + shaped_line.width;
8333 if x < fragment_end_x {
8334 return Some(
8335 fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
8336 );
8337 }
8338 fragment_start_x = fragment_end_x;
8339 fragment_start_index += shaped_line.len;
8340 }
8341 LineFragment::Element { len, size, .. } => {
8342 let fragment_end_x = fragment_start_x + size.width;
8343 if x < fragment_end_x {
8344 return Some(fragment_start_index);
8345 }
8346 fragment_start_index += len;
8347 fragment_start_x = fragment_end_x;
8348 }
8349 }
8350 }
8351
8352 None
8353 }
8354
8355 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
8356 let mut fragment_start_index = 0;
8357
8358 for fragment in &self.fragments {
8359 match fragment {
8360 LineFragment::Text(shaped_line) => {
8361 let fragment_end_index = fragment_start_index + shaped_line.len;
8362 if index < fragment_end_index {
8363 return shaped_line.font_id_for_index(index - fragment_start_index);
8364 }
8365 fragment_start_index = fragment_end_index;
8366 }
8367 LineFragment::Element { len, .. } => {
8368 let fragment_end_index = fragment_start_index + len;
8369 if index < fragment_end_index {
8370 return None;
8371 }
8372 fragment_start_index = fragment_end_index;
8373 }
8374 }
8375 }
8376
8377 None
8378 }
8379}
8380
8381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8382enum Invisible {
8383 /// A tab character
8384 ///
8385 /// A tab character is internally represented by spaces (configured by the user's tab width)
8386 /// aligned to the nearest column, so it's necessary to store the start and end offset for
8387 /// adjacency checks.
8388 Tab {
8389 line_start_offset: usize,
8390 line_end_offset: usize,
8391 },
8392 Whitespace {
8393 line_offset: usize,
8394 },
8395}
8396
8397impl EditorElement {
8398 /// Returns the rem size to use when rendering the [`EditorElement`].
8399 ///
8400 /// This allows UI elements to scale based on the `buffer_font_size`.
8401 fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
8402 match self.editor.read(cx).mode {
8403 EditorMode::Full {
8404 scale_ui_elements_with_buffer_font_size: true,
8405 ..
8406 }
8407 | EditorMode::Minimap { .. } => {
8408 let buffer_font_size = self.style.text.font_size;
8409 match buffer_font_size {
8410 AbsoluteLength::Pixels(pixels) => {
8411 let rem_size_scale = {
8412 // Our default UI font size is 14px on a 16px base scale.
8413 // This means the default UI font size is 0.875rems.
8414 let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
8415
8416 // We then determine the delta between a single rem and the default font
8417 // size scale.
8418 let default_font_size_delta = 1. - default_font_size_scale;
8419
8420 // Finally, we add this delta to 1rem to get the scale factor that
8421 // should be used to scale up the UI.
8422 1. + default_font_size_delta
8423 };
8424
8425 Some(pixels * rem_size_scale)
8426 }
8427 AbsoluteLength::Rems(rems) => {
8428 Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into()))
8429 }
8430 }
8431 }
8432 // We currently use single-line and auto-height editors in UI contexts,
8433 // so we don't want to scale everything with the buffer font size, as it
8434 // ends up looking off.
8435 _ => None,
8436 }
8437 }
8438
8439 fn editor_with_selections(&self, cx: &App) -> Option<Entity<Editor>> {
8440 if let EditorMode::Minimap { parent } = self.editor.read(cx).mode() {
8441 parent.upgrade()
8442 } else {
8443 Some(self.editor.clone())
8444 }
8445 }
8446}
8447
8448impl Element for EditorElement {
8449 type RequestLayoutState = ();
8450 type PrepaintState = EditorLayout;
8451
8452 fn id(&self) -> Option<ElementId> {
8453 None
8454 }
8455
8456 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
8457 None
8458 }
8459
8460 fn request_layout(
8461 &mut self,
8462 _: Option<&GlobalElementId>,
8463 _inspector_id: Option<&gpui::InspectorElementId>,
8464 window: &mut Window,
8465 cx: &mut App,
8466 ) -> (gpui::LayoutId, ()) {
8467 let rem_size = self.rem_size(cx);
8468 window.with_rem_size(rem_size, |window| {
8469 self.editor.update(cx, |editor, cx| {
8470 editor.set_style(self.style.clone(), window, cx);
8471
8472 let layout_id = match editor.mode {
8473 EditorMode::SingleLine => {
8474 let rem_size = window.rem_size();
8475 let height = self.style.text.line_height_in_pixels(rem_size);
8476 let mut style = Style::default();
8477 style.size.height = height.into();
8478 style.size.width = relative(1.).into();
8479 window.request_layout(style, None, cx)
8480 }
8481 EditorMode::AutoHeight {
8482 min_lines,
8483 max_lines,
8484 } => {
8485 let editor_handle = cx.entity();
8486 let max_line_number_width =
8487 self.max_line_number_width(&editor.snapshot(window, cx), window);
8488 window.request_measured_layout(
8489 Style::default(),
8490 move |known_dimensions, available_space, window, cx| {
8491 editor_handle
8492 .update(cx, |editor, cx| {
8493 compute_auto_height_layout(
8494 editor,
8495 min_lines,
8496 max_lines,
8497 max_line_number_width,
8498 known_dimensions,
8499 available_space.width,
8500 window,
8501 cx,
8502 )
8503 })
8504 .unwrap_or_default()
8505 },
8506 )
8507 }
8508 EditorMode::Minimap { .. } => {
8509 let mut style = Style::default();
8510 style.size.width = relative(1.).into();
8511 style.size.height = relative(1.).into();
8512 window.request_layout(style, None, cx)
8513 }
8514 EditorMode::Full {
8515 sizing_behavior, ..
8516 } => {
8517 let mut style = Style::default();
8518 style.size.width = relative(1.).into();
8519 if sizing_behavior == SizingBehavior::SizeByContent {
8520 let snapshot = editor.snapshot(window, cx);
8521 let line_height =
8522 self.style.text.line_height_in_pixels(window.rem_size());
8523 let scroll_height =
8524 (snapshot.max_point().row().next_row().0 as f32) * line_height;
8525 style.size.height = scroll_height.into();
8526 } else {
8527 style.size.height = relative(1.).into();
8528 }
8529 window.request_layout(style, None, cx)
8530 }
8531 };
8532
8533 (layout_id, ())
8534 })
8535 })
8536 }
8537
8538 fn prepaint(
8539 &mut self,
8540 _: Option<&GlobalElementId>,
8541 _inspector_id: Option<&gpui::InspectorElementId>,
8542 bounds: Bounds<Pixels>,
8543 _: &mut Self::RequestLayoutState,
8544 window: &mut Window,
8545 cx: &mut App,
8546 ) -> Self::PrepaintState {
8547 let text_style = TextStyleRefinement {
8548 font_size: Some(self.style.text.font_size),
8549 line_height: Some(self.style.text.line_height),
8550 ..Default::default()
8551 };
8552
8553 let is_minimap = self.editor.read(cx).mode.is_minimap();
8554
8555 if !is_minimap {
8556 let focus_handle = self.editor.focus_handle(cx);
8557 window.set_view_id(self.editor.entity_id());
8558 window.set_focus_handle(&focus_handle, cx);
8559 }
8560
8561 let rem_size = self.rem_size(cx);
8562 window.with_rem_size(rem_size, |window| {
8563 window.with_text_style(Some(text_style), |window| {
8564 window.with_content_mask(Some(ContentMask { bounds }), |window| {
8565 let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
8566 (editor.snapshot(window, cx), editor.read_only(cx))
8567 });
8568 let style = &self.style;
8569
8570 let rem_size = window.rem_size();
8571 let font_id = window.text_system().resolve_font(&style.text.font());
8572 let font_size = style.text.font_size.to_pixels(rem_size);
8573 let line_height = style.text.line_height_in_pixels(rem_size);
8574 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
8575 let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
8576 let glyph_grid_cell = size(em_advance, line_height);
8577
8578 let gutter_dimensions = snapshot
8579 .gutter_dimensions(
8580 font_id,
8581 font_size,
8582 self.max_line_number_width(&snapshot, window),
8583 cx,
8584 )
8585 .or_else(|| {
8586 self.editor.read(cx).offset_content.then(|| {
8587 GutterDimensions::default_with_margin(font_id, font_size, cx)
8588 })
8589 })
8590 .unwrap_or_default();
8591 let text_width = bounds.size.width - gutter_dimensions.width;
8592
8593 let settings = EditorSettings::get_global(cx);
8594 let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never;
8595 let vertical_scrollbar_width = (scrollbars_shown
8596 && settings.scrollbar.axes.vertical
8597 && self.editor.read(cx).show_scrollbars.vertical)
8598 .then_some(style.scrollbar_width)
8599 .unwrap_or_default();
8600 let minimap_width = self
8601 .get_minimap_width(
8602 &settings.minimap,
8603 scrollbars_shown,
8604 text_width,
8605 em_width,
8606 font_size,
8607 rem_size,
8608 cx,
8609 )
8610 .unwrap_or_default();
8611
8612 let right_margin = minimap_width + vertical_scrollbar_width;
8613
8614 let editor_width =
8615 text_width - gutter_dimensions.margin - 2 * em_width - right_margin;
8616 let editor_margins = EditorMargins {
8617 gutter: gutter_dimensions,
8618 right: right_margin,
8619 };
8620
8621 snapshot = self.editor.update(cx, |editor, cx| {
8622 editor.last_bounds = Some(bounds);
8623 editor.gutter_dimensions = gutter_dimensions;
8624 editor.set_visible_line_count(
8625 (bounds.size.height / line_height) as f64,
8626 window,
8627 cx,
8628 );
8629 editor.set_visible_column_count(f64::from(editor_width / em_advance));
8630
8631 if matches!(
8632 editor.mode,
8633 EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
8634 ) {
8635 snapshot
8636 } else {
8637 let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
8638 let wrap_width = match editor.soft_wrap_mode(cx) {
8639 SoftWrap::GitDiff => None,
8640 SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
8641 SoftWrap::EditorWidth => Some(editor_width),
8642 SoftWrap::Column(column) => Some(wrap_width_for(column)),
8643 SoftWrap::Bounded(column) => {
8644 Some(editor_width.min(wrap_width_for(column)))
8645 }
8646 };
8647
8648 if editor.set_wrap_width(wrap_width, cx) {
8649 editor.snapshot(window, cx)
8650 } else {
8651 snapshot
8652 }
8653 }
8654 });
8655
8656 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
8657 let gutter_hitbox = window.insert_hitbox(
8658 gutter_bounds(bounds, gutter_dimensions),
8659 HitboxBehavior::Normal,
8660 );
8661 let text_hitbox = window.insert_hitbox(
8662 Bounds {
8663 origin: gutter_hitbox.top_right(),
8664 size: size(text_width, bounds.size.height),
8665 },
8666 HitboxBehavior::Normal,
8667 );
8668
8669 // Offset the content_bounds from the text_bounds by the gutter margin (which
8670 // is roughly half a character wide) to make hit testing work more like how we want.
8671 let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO);
8672 let content_origin = text_hitbox.origin + content_offset;
8673
8674 let height_in_lines = f64::from(bounds.size.height / line_height);
8675 let max_row = snapshot.max_point().row().as_f64();
8676
8677 // The max scroll position for the top of the window
8678 let max_scroll_top = if matches!(
8679 snapshot.mode,
8680 EditorMode::SingleLine
8681 | EditorMode::AutoHeight { .. }
8682 | EditorMode::Full {
8683 sizing_behavior: SizingBehavior::ExcludeOverscrollMargin
8684 | SizingBehavior::SizeByContent,
8685 ..
8686 }
8687 ) {
8688 (max_row - height_in_lines + 1.).max(0.)
8689 } else {
8690 let settings = EditorSettings::get_global(cx);
8691 match settings.scroll_beyond_last_line {
8692 ScrollBeyondLastLine::OnePage => max_row,
8693 ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.).max(0.),
8694 ScrollBeyondLastLine::VerticalScrollMargin => {
8695 (max_row - height_in_lines + 1. + settings.vertical_scroll_margin)
8696 .max(0.)
8697 }
8698 }
8699 };
8700
8701 let (
8702 autoscroll_request,
8703 autoscroll_containing_element,
8704 needs_horizontal_autoscroll,
8705 ) = self.editor.update(cx, |editor, cx| {
8706 let autoscroll_request = editor.scroll_manager.take_autoscroll_request();
8707
8708 let autoscroll_containing_element =
8709 autoscroll_request.is_some() || editor.has_pending_selection();
8710
8711 let (needs_horizontal_autoscroll, was_scrolled) = editor
8712 .autoscroll_vertically(
8713 bounds,
8714 line_height,
8715 max_scroll_top,
8716 autoscroll_request,
8717 window,
8718 cx,
8719 );
8720 if was_scrolled.0 {
8721 snapshot = editor.snapshot(window, cx);
8722 }
8723 (
8724 autoscroll_request,
8725 autoscroll_containing_element,
8726 needs_horizontal_autoscroll,
8727 )
8728 });
8729
8730 let mut scroll_position = snapshot.scroll_position();
8731 // The scroll position is a fractional point, the whole number of which represents
8732 // the top of the window in terms of display rows.
8733 let start_row = DisplayRow(scroll_position.y as u32);
8734 let max_row = snapshot.max_point().row();
8735 let end_row = cmp::min(
8736 (scroll_position.y + height_in_lines).ceil() as u32,
8737 max_row.next_row().0,
8738 );
8739 let end_row = DisplayRow(end_row);
8740
8741 let row_infos = snapshot
8742 .row_infos(start_row)
8743 .take((start_row..end_row).len())
8744 .collect::<Vec<RowInfo>>();
8745 let is_row_soft_wrapped = |row: usize| {
8746 row_infos
8747 .get(row)
8748 .is_none_or(|info| info.buffer_row.is_none())
8749 };
8750
8751 let start_anchor = if start_row == Default::default() {
8752 Anchor::min()
8753 } else {
8754 snapshot.buffer_snapshot().anchor_before(
8755 DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
8756 )
8757 };
8758 let end_anchor = if end_row > max_row {
8759 Anchor::max()
8760 } else {
8761 snapshot.buffer_snapshot().anchor_before(
8762 DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
8763 )
8764 };
8765
8766 let mut highlighted_rows = self
8767 .editor
8768 .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
8769
8770 let is_light = cx.theme().appearance().is_light();
8771
8772 for (ix, row_info) in row_infos.iter().enumerate() {
8773 let Some(diff_status) = row_info.diff_status else {
8774 continue;
8775 };
8776
8777 let background_color = match diff_status.kind {
8778 DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
8779 DiffHunkStatusKind::Deleted => {
8780 cx.theme().colors().version_control_deleted
8781 }
8782 DiffHunkStatusKind::Modified => {
8783 debug_panic!("modified diff status for row info");
8784 continue;
8785 }
8786 };
8787
8788 let hunk_opacity = if is_light { 0.16 } else { 0.12 };
8789
8790 let hollow_highlight = LineHighlight {
8791 background: (background_color.opacity(if is_light {
8792 0.08
8793 } else {
8794 0.06
8795 }))
8796 .into(),
8797 border: Some(if is_light {
8798 background_color.opacity(0.48)
8799 } else {
8800 background_color.opacity(0.36)
8801 }),
8802 include_gutter: true,
8803 type_id: None,
8804 };
8805
8806 let filled_highlight = LineHighlight {
8807 background: solid_background(background_color.opacity(hunk_opacity)),
8808 border: None,
8809 include_gutter: true,
8810 type_id: None,
8811 };
8812
8813 let background = if Self::diff_hunk_hollow(diff_status, cx) {
8814 hollow_highlight
8815 } else {
8816 filled_highlight
8817 };
8818
8819 highlighted_rows
8820 .entry(start_row + DisplayRow(ix as u32))
8821 .or_insert(background);
8822 }
8823
8824 let highlighted_ranges = self
8825 .editor_with_selections(cx)
8826 .map(|editor| {
8827 editor.read(cx).background_highlights_in_range(
8828 start_anchor..end_anchor,
8829 &snapshot.display_snapshot,
8830 cx.theme(),
8831 )
8832 })
8833 .unwrap_or_default();
8834 let highlighted_gutter_ranges =
8835 self.editor.read(cx).gutter_highlights_in_range(
8836 start_anchor..end_anchor,
8837 &snapshot.display_snapshot,
8838 cx,
8839 );
8840
8841 let document_colors = self
8842 .editor
8843 .read(cx)
8844 .colors
8845 .as_ref()
8846 .map(|colors| colors.editor_display_highlights(&snapshot));
8847 let redacted_ranges = self.editor.read(cx).redacted_ranges(
8848 start_anchor..end_anchor,
8849 &snapshot.display_snapshot,
8850 cx,
8851 );
8852
8853 let (local_selections, selected_buffer_ids): (
8854 Vec<Selection<Point>>,
8855 Vec<BufferId>,
8856 ) = self
8857 .editor_with_selections(cx)
8858 .map(|editor| {
8859 editor.update(cx, |editor, cx| {
8860 let all_selections =
8861 editor.selections.all::<Point>(&snapshot.display_snapshot);
8862 let selected_buffer_ids =
8863 if editor.buffer_kind(cx) == ItemBufferKind::Singleton {
8864 Vec::new()
8865 } else {
8866 let mut selected_buffer_ids =
8867 Vec::with_capacity(all_selections.len());
8868
8869 for selection in all_selections {
8870 for buffer_id in snapshot
8871 .buffer_snapshot()
8872 .buffer_ids_for_range(selection.range())
8873 {
8874 if selected_buffer_ids.last() != Some(&buffer_id) {
8875 selected_buffer_ids.push(buffer_id);
8876 }
8877 }
8878 }
8879
8880 selected_buffer_ids
8881 };
8882
8883 let mut selections = editor.selections.disjoint_in_range(
8884 start_anchor..end_anchor,
8885 &snapshot.display_snapshot,
8886 );
8887 selections
8888 .extend(editor.selections.pending(&snapshot.display_snapshot));
8889
8890 (selections, selected_buffer_ids)
8891 })
8892 })
8893 .unwrap_or_default();
8894
8895 let (selections, mut active_rows, newest_selection_head) = self
8896 .layout_selections(
8897 start_anchor,
8898 end_anchor,
8899 &local_selections,
8900 &snapshot,
8901 start_row,
8902 end_row,
8903 window,
8904 cx,
8905 );
8906 let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
8907 editor.active_breakpoints(start_row..end_row, window, cx)
8908 });
8909 for (display_row, (_, bp, state)) in &breakpoint_rows {
8910 if bp.is_enabled() && state.is_none_or(|s| s.verified) {
8911 active_rows.entry(*display_row).or_default().breakpoint = true;
8912 }
8913 }
8914
8915 let line_numbers = self.layout_line_numbers(
8916 Some(&gutter_hitbox),
8917 gutter_dimensions,
8918 line_height,
8919 scroll_position,
8920 start_row..end_row,
8921 &row_infos,
8922 &active_rows,
8923 newest_selection_head,
8924 &snapshot,
8925 window,
8926 cx,
8927 );
8928
8929 // We add the gutter breakpoint indicator to breakpoint_rows after painting
8930 // line numbers so we don't paint a line number debug accent color if a user
8931 // has their mouse over that line when a breakpoint isn't there
8932 self.editor.update(cx, |editor, _| {
8933 if let Some(phantom_breakpoint) = &mut editor
8934 .gutter_breakpoint_indicator
8935 .0
8936 .filter(|phantom_breakpoint| phantom_breakpoint.is_active)
8937 {
8938 // Is there a non-phantom breakpoint on this line?
8939 phantom_breakpoint.collides_with_existing_breakpoint = true;
8940 breakpoint_rows
8941 .entry(phantom_breakpoint.display_row)
8942 .or_insert_with(|| {
8943 let position = snapshot.display_point_to_anchor(
8944 DisplayPoint::new(phantom_breakpoint.display_row, 0),
8945 Bias::Right,
8946 );
8947 let breakpoint = Breakpoint::new_standard();
8948 phantom_breakpoint.collides_with_existing_breakpoint = false;
8949 (position, breakpoint, None)
8950 });
8951 }
8952 });
8953
8954 let mut expand_toggles =
8955 window.with_element_namespace("expand_toggles", |window| {
8956 self.layout_expand_toggles(
8957 &gutter_hitbox,
8958 gutter_dimensions,
8959 em_width,
8960 line_height,
8961 scroll_position,
8962 &row_infos,
8963 window,
8964 cx,
8965 )
8966 });
8967
8968 let mut crease_toggles =
8969 window.with_element_namespace("crease_toggles", |window| {
8970 self.layout_crease_toggles(
8971 start_row..end_row,
8972 &row_infos,
8973 &active_rows,
8974 &snapshot,
8975 window,
8976 cx,
8977 )
8978 });
8979 let crease_trailers =
8980 window.with_element_namespace("crease_trailers", |window| {
8981 self.layout_crease_trailers(
8982 row_infos.iter().copied(),
8983 &snapshot,
8984 window,
8985 cx,
8986 )
8987 });
8988
8989 let display_hunks = self.layout_gutter_diff_hunks(
8990 line_height,
8991 &gutter_hitbox,
8992 start_row..end_row,
8993 &snapshot,
8994 window,
8995 cx,
8996 );
8997
8998 let merged_highlighted_ranges =
8999 if let Some((_, colors)) = document_colors.as_ref() {
9000 &highlighted_ranges
9001 .clone()
9002 .into_iter()
9003 .chain(colors.clone())
9004 .collect()
9005 } else {
9006 &highlighted_ranges
9007 };
9008 let bg_segments_per_row = Self::bg_segments_per_row(
9009 start_row..end_row,
9010 &selections,
9011 &merged_highlighted_ranges,
9012 self.style.background,
9013 );
9014
9015 let mut line_layouts = Self::layout_lines(
9016 start_row..end_row,
9017 &snapshot,
9018 &self.style,
9019 editor_width,
9020 is_row_soft_wrapped,
9021 &bg_segments_per_row,
9022 window,
9023 cx,
9024 );
9025 let new_renderer_widths = (!is_minimap).then(|| {
9026 line_layouts
9027 .iter()
9028 .flat_map(|layout| &layout.fragments)
9029 .filter_map(|fragment| {
9030 if let LineFragment::Element { id, size, .. } = fragment {
9031 Some((*id, size.width))
9032 } else {
9033 None
9034 }
9035 })
9036 });
9037 if new_renderer_widths.is_some_and(|new_renderer_widths| {
9038 self.editor.update(cx, |editor, cx| {
9039 editor.update_renderer_widths(new_renderer_widths, cx)
9040 })
9041 }) {
9042 // If the fold widths have changed, we need to prepaint
9043 // the element again to account for any changes in
9044 // wrapping.
9045 return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
9046 }
9047
9048 let longest_line_blame_width = self
9049 .editor
9050 .update(cx, |editor, cx| {
9051 if !editor.show_git_blame_inline {
9052 return None;
9053 }
9054 let blame = editor.blame.as_ref()?;
9055 let (_, blame_entry) = blame
9056 .update(cx, |blame, cx| {
9057 let row_infos =
9058 snapshot.row_infos(snapshot.longest_row()).next()?;
9059 blame.blame_for_rows(&[row_infos], cx).next()
9060 })
9061 .flatten()?;
9062 let mut element = render_inline_blame_entry(blame_entry, style, cx)?;
9063 let inline_blame_padding =
9064 ProjectSettings::get_global(cx).git.inline_blame.padding as f32
9065 * em_advance;
9066 Some(
9067 element
9068 .layout_as_root(AvailableSpace::min_size(), window, cx)
9069 .width
9070 + inline_blame_padding,
9071 )
9072 })
9073 .unwrap_or(Pixels::ZERO);
9074
9075 let longest_line_width = layout_line(
9076 snapshot.longest_row(),
9077 &snapshot,
9078 style,
9079 editor_width,
9080 is_row_soft_wrapped,
9081 window,
9082 cx,
9083 )
9084 .width;
9085
9086 let scrollbar_layout_information = ScrollbarLayoutInformation::new(
9087 text_hitbox.bounds,
9088 glyph_grid_cell,
9089 size(
9090 longest_line_width,
9091 Pixels::from(max_row.as_f64() * f64::from(line_height)),
9092 ),
9093 longest_line_blame_width,
9094 EditorSettings::get_global(cx),
9095 );
9096
9097 let mut scroll_width = scrollbar_layout_information.scroll_range.width;
9098
9099 let sticky_header_excerpt = if snapshot.buffer_snapshot().show_headers() {
9100 snapshot.sticky_header_excerpt(scroll_position.y)
9101 } else {
9102 None
9103 };
9104 let sticky_header_excerpt_id =
9105 sticky_header_excerpt.as_ref().map(|top| top.excerpt.id);
9106
9107 let blocks = (!is_minimap)
9108 .then(|| {
9109 window.with_element_namespace("blocks", |window| {
9110 self.render_blocks(
9111 start_row..end_row,
9112 &snapshot,
9113 &hitbox,
9114 &text_hitbox,
9115 editor_width,
9116 &mut scroll_width,
9117 &editor_margins,
9118 em_width,
9119 gutter_dimensions.full_width(),
9120 line_height,
9121 &mut line_layouts,
9122 &local_selections,
9123 &selected_buffer_ids,
9124 is_row_soft_wrapped,
9125 sticky_header_excerpt_id,
9126 window,
9127 cx,
9128 )
9129 })
9130 })
9131 .unwrap_or_else(|| Ok((Vec::default(), HashMap::default())));
9132 let (mut blocks, row_block_types) = match blocks {
9133 Ok(blocks) => blocks,
9134 Err(resized_blocks) => {
9135 self.editor.update(cx, |editor, cx| {
9136 editor.resize_blocks(
9137 resized_blocks,
9138 autoscroll_request.map(|(autoscroll, _)| autoscroll),
9139 cx,
9140 )
9141 });
9142 return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
9143 }
9144 };
9145
9146 let sticky_buffer_header = sticky_header_excerpt.map(|sticky_header_excerpt| {
9147 window.with_element_namespace("blocks", |window| {
9148 self.layout_sticky_buffer_header(
9149 sticky_header_excerpt,
9150 scroll_position,
9151 line_height,
9152 right_margin,
9153 &snapshot,
9154 &hitbox,
9155 &selected_buffer_ids,
9156 &blocks,
9157 window,
9158 cx,
9159 )
9160 })
9161 });
9162
9163 let start_buffer_row =
9164 MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot()).row);
9165 let end_buffer_row =
9166 MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot()).row);
9167
9168 let scroll_max: gpui::Point<ScrollPixelOffset> = point(
9169 ScrollPixelOffset::from(
9170 ((scroll_width - editor_width) / em_advance).max(0.0),
9171 ),
9172 max_scroll_top,
9173 );
9174
9175 self.editor.update(cx, |editor, cx| {
9176 if editor.scroll_manager.clamp_scroll_left(scroll_max.x) {
9177 scroll_position.x = scroll_position.x.min(scroll_max.x);
9178 }
9179
9180 if needs_horizontal_autoscroll.0
9181 && let Some(new_scroll_position) = editor.autoscroll_horizontally(
9182 start_row,
9183 editor_width,
9184 scroll_width,
9185 em_advance,
9186 &line_layouts,
9187 autoscroll_request,
9188 window,
9189 cx,
9190 )
9191 {
9192 scroll_position = new_scroll_position;
9193 }
9194 });
9195
9196 let scroll_pixel_position = point(
9197 scroll_position.x * f64::from(em_advance),
9198 scroll_position.y * f64::from(line_height),
9199 );
9200 let indent_guides = self.layout_indent_guides(
9201 content_origin,
9202 text_hitbox.origin,
9203 start_buffer_row..end_buffer_row,
9204 scroll_pixel_position,
9205 line_height,
9206 &snapshot,
9207 window,
9208 cx,
9209 );
9210
9211 let crease_trailers =
9212 window.with_element_namespace("crease_trailers", |window| {
9213 self.prepaint_crease_trailers(
9214 crease_trailers,
9215 &line_layouts,
9216 line_height,
9217 content_origin,
9218 scroll_pixel_position,
9219 em_width,
9220 window,
9221 cx,
9222 )
9223 });
9224
9225 let (edit_prediction_popover, edit_prediction_popover_origin) = self
9226 .editor
9227 .update(cx, |editor, cx| {
9228 editor.render_edit_prediction_popover(
9229 &text_hitbox.bounds,
9230 content_origin,
9231 right_margin,
9232 &snapshot,
9233 start_row..end_row,
9234 scroll_position.y,
9235 scroll_position.y + height_in_lines,
9236 &line_layouts,
9237 line_height,
9238 scroll_position,
9239 scroll_pixel_position,
9240 newest_selection_head,
9241 editor_width,
9242 style,
9243 window,
9244 cx,
9245 )
9246 })
9247 .unzip();
9248
9249 let mut inline_diagnostics = self.layout_inline_diagnostics(
9250 &line_layouts,
9251 &crease_trailers,
9252 &row_block_types,
9253 content_origin,
9254 scroll_position,
9255 scroll_pixel_position,
9256 edit_prediction_popover_origin,
9257 start_row,
9258 end_row,
9259 line_height,
9260 em_width,
9261 style,
9262 window,
9263 cx,
9264 );
9265
9266 let mut inline_blame_layout = None;
9267 let mut inline_code_actions = None;
9268 if let Some(newest_selection_head) = newest_selection_head {
9269 let display_row = newest_selection_head.row();
9270 if (start_row..end_row).contains(&display_row)
9271 && !row_block_types.contains_key(&display_row)
9272 {
9273 inline_code_actions = self.layout_inline_code_actions(
9274 newest_selection_head,
9275 content_origin,
9276 scroll_position,
9277 scroll_pixel_position,
9278 line_height,
9279 &snapshot,
9280 window,
9281 cx,
9282 );
9283
9284 let line_ix = display_row.minus(start_row) as usize;
9285 if let (Some(row_info), Some(line_layout), Some(crease_trailer)) = (
9286 row_infos.get(line_ix),
9287 line_layouts.get(line_ix),
9288 crease_trailers.get(line_ix),
9289 ) {
9290 let crease_trailer_layout = crease_trailer.as_ref();
9291 if let Some(layout) = self.layout_inline_blame(
9292 display_row,
9293 row_info,
9294 line_layout,
9295 crease_trailer_layout,
9296 em_width,
9297 content_origin,
9298 scroll_position,
9299 scroll_pixel_position,
9300 line_height,
9301 &text_hitbox,
9302 window,
9303 cx,
9304 ) {
9305 inline_blame_layout = Some(layout);
9306 // Blame overrides inline diagnostics
9307 inline_diagnostics.remove(&display_row);
9308 }
9309 } else {
9310 log::error!(
9311 "bug: line_ix {} is out of bounds - row_infos.len(): {}, \
9312 line_layouts.len(): {}, \
9313 crease_trailers.len(): {}",
9314 line_ix,
9315 row_infos.len(),
9316 line_layouts.len(),
9317 crease_trailers.len(),
9318 );
9319 }
9320 }
9321 }
9322
9323 let blamed_display_rows = self.layout_blame_entries(
9324 &row_infos,
9325 em_width,
9326 scroll_position,
9327 line_height,
9328 &gutter_hitbox,
9329 gutter_dimensions.git_blame_entries_width,
9330 window,
9331 cx,
9332 );
9333
9334 let line_elements = self.prepaint_lines(
9335 start_row,
9336 &mut line_layouts,
9337 line_height,
9338 scroll_position,
9339 scroll_pixel_position,
9340 content_origin,
9341 window,
9342 cx,
9343 );
9344
9345 window.with_element_namespace("blocks", |window| {
9346 self.layout_blocks(
9347 &mut blocks,
9348 &hitbox,
9349 line_height,
9350 scroll_position,
9351 scroll_pixel_position,
9352 window,
9353 cx,
9354 );
9355 });
9356
9357 let cursors = self.collect_cursors(&snapshot, cx);
9358 let visible_row_range = start_row..end_row;
9359 let non_visible_cursors = cursors
9360 .iter()
9361 .any(|c| !visible_row_range.contains(&c.0.row()));
9362
9363 let visible_cursors = self.layout_visible_cursors(
9364 &snapshot,
9365 &selections,
9366 &row_block_types,
9367 start_row..end_row,
9368 &line_layouts,
9369 &text_hitbox,
9370 content_origin,
9371 scroll_position,
9372 scroll_pixel_position,
9373 line_height,
9374 em_width,
9375 em_advance,
9376 autoscroll_containing_element,
9377 window,
9378 cx,
9379 );
9380
9381 let scrollbars_layout = self.layout_scrollbars(
9382 &snapshot,
9383 &scrollbar_layout_information,
9384 content_offset,
9385 scroll_position,
9386 non_visible_cursors,
9387 right_margin,
9388 editor_width,
9389 window,
9390 cx,
9391 );
9392
9393 let gutter_settings = EditorSettings::get_global(cx).gutter;
9394
9395 let context_menu_layout =
9396 if let Some(newest_selection_head) = newest_selection_head {
9397 let newest_selection_point =
9398 newest_selection_head.to_point(&snapshot.display_snapshot);
9399 if (start_row..end_row).contains(&newest_selection_head.row()) {
9400 self.layout_cursor_popovers(
9401 line_height,
9402 &text_hitbox,
9403 content_origin,
9404 right_margin,
9405 start_row,
9406 scroll_pixel_position,
9407 &line_layouts,
9408 newest_selection_head,
9409 newest_selection_point,
9410 style,
9411 window,
9412 cx,
9413 )
9414 } else {
9415 None
9416 }
9417 } else {
9418 None
9419 };
9420
9421 self.layout_gutter_menu(
9422 line_height,
9423 &text_hitbox,
9424 content_origin,
9425 right_margin,
9426 scroll_pixel_position,
9427 gutter_dimensions.width - gutter_dimensions.left_padding,
9428 window,
9429 cx,
9430 );
9431
9432 let test_indicators = if gutter_settings.runnables {
9433 self.layout_run_indicators(
9434 line_height,
9435 start_row..end_row,
9436 &row_infos,
9437 scroll_position,
9438 &gutter_dimensions,
9439 &gutter_hitbox,
9440 &display_hunks,
9441 &snapshot,
9442 &mut breakpoint_rows,
9443 window,
9444 cx,
9445 )
9446 } else {
9447 Vec::new()
9448 };
9449
9450 let show_breakpoints = snapshot
9451 .show_breakpoints
9452 .unwrap_or(gutter_settings.breakpoints);
9453 let breakpoints = if show_breakpoints {
9454 self.layout_breakpoints(
9455 line_height,
9456 start_row..end_row,
9457 scroll_position,
9458 &gutter_dimensions,
9459 &gutter_hitbox,
9460 &display_hunks,
9461 &snapshot,
9462 breakpoint_rows,
9463 &row_infos,
9464 window,
9465 cx,
9466 )
9467 } else {
9468 Vec::new()
9469 };
9470
9471 self.layout_signature_help(
9472 &hitbox,
9473 content_origin,
9474 scroll_pixel_position,
9475 newest_selection_head,
9476 start_row,
9477 &line_layouts,
9478 line_height,
9479 em_width,
9480 context_menu_layout,
9481 window,
9482 cx,
9483 );
9484
9485 if !cx.has_active_drag() {
9486 self.layout_hover_popovers(
9487 &snapshot,
9488 &hitbox,
9489 start_row..end_row,
9490 content_origin,
9491 scroll_pixel_position,
9492 &line_layouts,
9493 line_height,
9494 em_width,
9495 context_menu_layout,
9496 window,
9497 cx,
9498 );
9499 }
9500
9501 let mouse_context_menu = self.layout_mouse_context_menu(
9502 &snapshot,
9503 start_row..end_row,
9504 content_origin,
9505 window,
9506 cx,
9507 );
9508
9509 window.with_element_namespace("crease_toggles", |window| {
9510 self.prepaint_crease_toggles(
9511 &mut crease_toggles,
9512 line_height,
9513 &gutter_dimensions,
9514 gutter_settings,
9515 scroll_pixel_position,
9516 &gutter_hitbox,
9517 window,
9518 cx,
9519 )
9520 });
9521
9522 window.with_element_namespace("expand_toggles", |window| {
9523 self.prepaint_expand_toggles(&mut expand_toggles, window, cx)
9524 });
9525
9526 let wrap_guides = self.layout_wrap_guides(
9527 em_advance,
9528 scroll_position,
9529 content_origin,
9530 scrollbars_layout.as_ref(),
9531 vertical_scrollbar_width,
9532 &hitbox,
9533 window,
9534 cx,
9535 );
9536
9537 let minimap = window.with_element_namespace("minimap", |window| {
9538 self.layout_minimap(
9539 &snapshot,
9540 minimap_width,
9541 scroll_position,
9542 &scrollbar_layout_information,
9543 scrollbars_layout.as_ref(),
9544 window,
9545 cx,
9546 )
9547 });
9548
9549 let invisible_symbol_font_size = font_size / 2.;
9550 let whitespace_map = &self
9551 .editor
9552 .read(cx)
9553 .buffer
9554 .read(cx)
9555 .language_settings(cx)
9556 .whitespace_map;
9557
9558 let tab_char = whitespace_map.tab.clone();
9559 let tab_len = tab_char.len();
9560 let tab_invisible = window.text_system().shape_line(
9561 tab_char,
9562 invisible_symbol_font_size,
9563 &[TextRun {
9564 len: tab_len,
9565 font: self.style.text.font(),
9566 color: cx.theme().colors().editor_invisible,
9567 ..Default::default()
9568 }],
9569 None,
9570 );
9571
9572 let space_char = whitespace_map.space.clone();
9573 let space_len = space_char.len();
9574 let space_invisible = window.text_system().shape_line(
9575 space_char,
9576 invisible_symbol_font_size,
9577 &[TextRun {
9578 len: space_len,
9579 font: self.style.text.font(),
9580 color: cx.theme().colors().editor_invisible,
9581 ..Default::default()
9582 }],
9583 None,
9584 );
9585
9586 let mode = snapshot.mode.clone();
9587
9588 let (diff_hunk_controls, diff_hunk_control_bounds) = if is_read_only {
9589 (vec![], vec![])
9590 } else {
9591 self.layout_diff_hunk_controls(
9592 start_row..end_row,
9593 &row_infos,
9594 &text_hitbox,
9595 newest_selection_head,
9596 line_height,
9597 right_margin,
9598 scroll_pixel_position,
9599 &display_hunks,
9600 &highlighted_rows,
9601 self.editor.clone(),
9602 window,
9603 cx,
9604 )
9605 };
9606
9607 let position_map = Rc::new(PositionMap {
9608 size: bounds.size,
9609 visible_row_range,
9610 scroll_position,
9611 scroll_pixel_position,
9612 scroll_max,
9613 line_layouts,
9614 line_height,
9615 em_width,
9616 em_advance,
9617 snapshot,
9618 gutter_hitbox: gutter_hitbox.clone(),
9619 text_hitbox: text_hitbox.clone(),
9620 inline_blame_bounds: inline_blame_layout
9621 .as_ref()
9622 .map(|layout| (layout.bounds, layout.buffer_id, layout.entry.clone())),
9623 display_hunks: display_hunks.clone(),
9624 diff_hunk_control_bounds,
9625 });
9626
9627 self.editor.update(cx, |editor, _| {
9628 editor.last_position_map = Some(position_map.clone())
9629 });
9630
9631 EditorLayout {
9632 mode,
9633 position_map,
9634 visible_display_row_range: start_row..end_row,
9635 wrap_guides,
9636 indent_guides,
9637 hitbox,
9638 gutter_hitbox,
9639 display_hunks,
9640 content_origin,
9641 scrollbars_layout,
9642 minimap,
9643 active_rows,
9644 highlighted_rows,
9645 highlighted_ranges,
9646 highlighted_gutter_ranges,
9647 redacted_ranges,
9648 document_colors,
9649 line_elements,
9650 line_numbers,
9651 blamed_display_rows,
9652 inline_diagnostics,
9653 inline_blame_layout,
9654 inline_code_actions,
9655 blocks,
9656 cursors,
9657 visible_cursors,
9658 selections,
9659 edit_prediction_popover,
9660 diff_hunk_controls,
9661 mouse_context_menu,
9662 test_indicators,
9663 breakpoints,
9664 crease_toggles,
9665 crease_trailers,
9666 tab_invisible,
9667 space_invisible,
9668 sticky_buffer_header,
9669 expand_toggles,
9670 }
9671 })
9672 })
9673 })
9674 }
9675
9676 fn paint(
9677 &mut self,
9678 _: Option<&GlobalElementId>,
9679 _inspector_id: Option<&gpui::InspectorElementId>,
9680 bounds: Bounds<gpui::Pixels>,
9681 _: &mut Self::RequestLayoutState,
9682 layout: &mut Self::PrepaintState,
9683 window: &mut Window,
9684 cx: &mut App,
9685 ) {
9686 if !layout.mode.is_minimap() {
9687 let focus_handle = self.editor.focus_handle(cx);
9688 let key_context = self
9689 .editor
9690 .update(cx, |editor, cx| editor.key_context(window, cx));
9691
9692 window.set_key_context(key_context);
9693 window.handle_input(
9694 &focus_handle,
9695 ElementInputHandler::new(bounds, self.editor.clone()),
9696 cx,
9697 );
9698 self.register_actions(window, cx);
9699 self.register_key_listeners(window, cx, layout);
9700 }
9701
9702 let text_style = TextStyleRefinement {
9703 font_size: Some(self.style.text.font_size),
9704 line_height: Some(self.style.text.line_height),
9705 ..Default::default()
9706 };
9707 let rem_size = self.rem_size(cx);
9708 window.with_rem_size(rem_size, |window| {
9709 window.with_text_style(Some(text_style), |window| {
9710 window.with_content_mask(Some(ContentMask { bounds }), |window| {
9711 self.paint_mouse_listeners(layout, window, cx);
9712 self.paint_background(layout, window, cx);
9713 self.paint_indent_guides(layout, window, cx);
9714
9715 if layout.gutter_hitbox.size.width > Pixels::ZERO {
9716 self.paint_blamed_display_rows(layout, window, cx);
9717 self.paint_line_numbers(layout, window, cx);
9718 }
9719
9720 self.paint_text(layout, window, cx);
9721
9722 if layout.gutter_hitbox.size.width > Pixels::ZERO {
9723 self.paint_gutter_highlights(layout, window, cx);
9724 self.paint_gutter_indicators(layout, window, cx);
9725 }
9726
9727 if !layout.blocks.is_empty() {
9728 window.with_element_namespace("blocks", |window| {
9729 self.paint_blocks(layout, window, cx);
9730 });
9731 }
9732
9733 window.with_element_namespace("blocks", |window| {
9734 if let Some(mut sticky_header) = layout.sticky_buffer_header.take() {
9735 sticky_header.paint(window, cx)
9736 }
9737 });
9738
9739 self.paint_minimap(layout, window, cx);
9740 self.paint_scrollbars(layout, window, cx);
9741 self.paint_edit_prediction_popover(layout, window, cx);
9742 self.paint_mouse_context_menu(layout, window, cx);
9743 });
9744 })
9745 })
9746 }
9747}
9748
9749pub(super) fn gutter_bounds(
9750 editor_bounds: Bounds<Pixels>,
9751 gutter_dimensions: GutterDimensions,
9752) -> Bounds<Pixels> {
9753 Bounds {
9754 origin: editor_bounds.origin,
9755 size: size(gutter_dimensions.width, editor_bounds.size.height),
9756 }
9757}
9758
9759#[derive(Clone, Copy)]
9760struct ContextMenuLayout {
9761 y_flipped: bool,
9762 bounds: Bounds<Pixels>,
9763}
9764
9765/// Holds information required for layouting the editor scrollbars.
9766struct ScrollbarLayoutInformation {
9767 /// The bounds of the editor area (excluding the content offset).
9768 editor_bounds: Bounds<Pixels>,
9769 /// The available range to scroll within the document.
9770 scroll_range: Size<Pixels>,
9771 /// The space available for one glyph in the editor.
9772 glyph_grid_cell: Size<Pixels>,
9773}
9774
9775impl ScrollbarLayoutInformation {
9776 pub fn new(
9777 editor_bounds: Bounds<Pixels>,
9778 glyph_grid_cell: Size<Pixels>,
9779 document_size: Size<Pixels>,
9780 longest_line_blame_width: Pixels,
9781 settings: &EditorSettings,
9782 ) -> Self {
9783 let vertical_overscroll = match settings.scroll_beyond_last_line {
9784 ScrollBeyondLastLine::OnePage => editor_bounds.size.height,
9785 ScrollBeyondLastLine::Off => glyph_grid_cell.height,
9786 ScrollBeyondLastLine::VerticalScrollMargin => {
9787 (1.0 + settings.vertical_scroll_margin) as f32 * glyph_grid_cell.height
9788 }
9789 };
9790
9791 let overscroll = size(longest_line_blame_width, vertical_overscroll);
9792
9793 ScrollbarLayoutInformation {
9794 editor_bounds,
9795 scroll_range: document_size + overscroll,
9796 glyph_grid_cell,
9797 }
9798 }
9799}
9800
9801impl IntoElement for EditorElement {
9802 type Element = Self;
9803
9804 fn into_element(self) -> Self::Element {
9805 self
9806 }
9807}
9808
9809pub struct EditorLayout {
9810 position_map: Rc<PositionMap>,
9811 hitbox: Hitbox,
9812 gutter_hitbox: Hitbox,
9813 content_origin: gpui::Point<Pixels>,
9814 scrollbars_layout: Option<EditorScrollbars>,
9815 minimap: Option<MinimapLayout>,
9816 mode: EditorMode,
9817 wrap_guides: SmallVec<[(Pixels, bool); 2]>,
9818 indent_guides: Option<Vec<IndentGuideLayout>>,
9819 visible_display_row_range: Range<DisplayRow>,
9820 active_rows: BTreeMap<DisplayRow, LineHighlightSpec>,
9821 highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
9822 line_elements: SmallVec<[AnyElement; 1]>,
9823 line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
9824 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
9825 blamed_display_rows: Option<Vec<AnyElement>>,
9826 inline_diagnostics: HashMap<DisplayRow, AnyElement>,
9827 inline_blame_layout: Option<InlineBlameLayout>,
9828 inline_code_actions: Option<AnyElement>,
9829 blocks: Vec<BlockLayout>,
9830 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
9831 highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
9832 redacted_ranges: Vec<Range<DisplayPoint>>,
9833 cursors: Vec<(DisplayPoint, Hsla)>,
9834 visible_cursors: Vec<CursorLayout>,
9835 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
9836 test_indicators: Vec<AnyElement>,
9837 breakpoints: Vec<AnyElement>,
9838 crease_toggles: Vec<Option<AnyElement>>,
9839 expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,
9840 diff_hunk_controls: Vec<AnyElement>,
9841 crease_trailers: Vec<Option<CreaseTrailerLayout>>,
9842 edit_prediction_popover: Option<AnyElement>,
9843 mouse_context_menu: Option<AnyElement>,
9844 tab_invisible: ShapedLine,
9845 space_invisible: ShapedLine,
9846 sticky_buffer_header: Option<AnyElement>,
9847 document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
9848}
9849
9850impl EditorLayout {
9851 fn line_end_overshoot(&self) -> Pixels {
9852 0.15 * self.position_map.line_height
9853 }
9854}
9855
9856#[derive(Debug)]
9857struct LineNumberSegment {
9858 shaped_line: ShapedLine,
9859 hitbox: Option<Hitbox>,
9860}
9861
9862#[derive(Debug)]
9863struct LineNumberLayout {
9864 segments: SmallVec<[LineNumberSegment; 1]>,
9865}
9866
9867struct ColoredRange<T> {
9868 start: T,
9869 end: T,
9870 color: Hsla,
9871}
9872
9873impl Along for ScrollbarAxes {
9874 type Unit = bool;
9875
9876 fn along(&self, axis: ScrollbarAxis) -> Self::Unit {
9877 match axis {
9878 ScrollbarAxis::Horizontal => self.horizontal,
9879 ScrollbarAxis::Vertical => self.vertical,
9880 }
9881 }
9882
9883 fn apply_along(&self, axis: ScrollbarAxis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self {
9884 match axis {
9885 ScrollbarAxis::Horizontal => ScrollbarAxes {
9886 horizontal: f(self.horizontal),
9887 vertical: self.vertical,
9888 },
9889 ScrollbarAxis::Vertical => ScrollbarAxes {
9890 horizontal: self.horizontal,
9891 vertical: f(self.vertical),
9892 },
9893 }
9894 }
9895}
9896
9897#[derive(Clone)]
9898struct EditorScrollbars {
9899 pub vertical: Option<ScrollbarLayout>,
9900 pub horizontal: Option<ScrollbarLayout>,
9901 pub visible: bool,
9902}
9903
9904impl EditorScrollbars {
9905 pub fn from_scrollbar_axes(
9906 show_scrollbar: ScrollbarAxes,
9907 layout_information: &ScrollbarLayoutInformation,
9908 content_offset: gpui::Point<Pixels>,
9909 scroll_position: gpui::Point<f64>,
9910 scrollbar_width: Pixels,
9911 right_margin: Pixels,
9912 editor_width: Pixels,
9913 show_scrollbars: bool,
9914 scrollbar_state: Option<&ActiveScrollbarState>,
9915 window: &mut Window,
9916 ) -> Self {
9917 let ScrollbarLayoutInformation {
9918 editor_bounds,
9919 scroll_range,
9920 glyph_grid_cell,
9921 } = layout_information;
9922
9923 let viewport_size = size(editor_width, editor_bounds.size.height);
9924
9925 let scrollbar_bounds_for = |axis: ScrollbarAxis| match axis {
9926 ScrollbarAxis::Horizontal => Bounds::from_corner_and_size(
9927 Corner::BottomLeft,
9928 editor_bounds.bottom_left(),
9929 size(
9930 // The horizontal viewport size differs from the space available for the
9931 // horizontal scrollbar, so we have to manually stitch it together here.
9932 editor_bounds.size.width - right_margin,
9933 scrollbar_width,
9934 ),
9935 ),
9936 ScrollbarAxis::Vertical => Bounds::from_corner_and_size(
9937 Corner::TopRight,
9938 editor_bounds.top_right(),
9939 size(scrollbar_width, viewport_size.height),
9940 ),
9941 };
9942
9943 let mut create_scrollbar_layout = |axis| {
9944 let viewport_size = viewport_size.along(axis);
9945 let scroll_range = scroll_range.along(axis);
9946
9947 // We always want a vertical scrollbar track for scrollbar diagnostic visibility.
9948 (show_scrollbar.along(axis)
9949 && (axis == ScrollbarAxis::Vertical || scroll_range > viewport_size))
9950 .then(|| {
9951 ScrollbarLayout::new(
9952 window.insert_hitbox(scrollbar_bounds_for(axis), HitboxBehavior::Normal),
9953 viewport_size,
9954 scroll_range,
9955 glyph_grid_cell.along(axis),
9956 content_offset.along(axis),
9957 scroll_position.along(axis),
9958 show_scrollbars,
9959 axis,
9960 )
9961 .with_thumb_state(
9962 scrollbar_state.and_then(|state| state.thumb_state_for_axis(axis)),
9963 )
9964 })
9965 };
9966
9967 Self {
9968 vertical: create_scrollbar_layout(ScrollbarAxis::Vertical),
9969 horizontal: create_scrollbar_layout(ScrollbarAxis::Horizontal),
9970 visible: show_scrollbars,
9971 }
9972 }
9973
9974 pub fn iter_scrollbars(&self) -> impl Iterator<Item = (&ScrollbarLayout, ScrollbarAxis)> + '_ {
9975 [
9976 (&self.vertical, ScrollbarAxis::Vertical),
9977 (&self.horizontal, ScrollbarAxis::Horizontal),
9978 ]
9979 .into_iter()
9980 .filter_map(|(scrollbar, axis)| scrollbar.as_ref().map(|s| (s, axis)))
9981 }
9982
9983 /// Returns the currently hovered scrollbar axis, if any.
9984 pub fn get_hovered_axis(&self, window: &Window) -> Option<(&ScrollbarLayout, ScrollbarAxis)> {
9985 self.iter_scrollbars()
9986 .find(|s| s.0.hitbox.is_hovered(window))
9987 }
9988}
9989
9990#[derive(Clone)]
9991struct ScrollbarLayout {
9992 hitbox: Hitbox,
9993 visible_range: Range<ScrollOffset>,
9994 text_unit_size: Pixels,
9995 thumb_bounds: Option<Bounds<Pixels>>,
9996 thumb_state: ScrollbarThumbState,
9997}
9998
9999impl ScrollbarLayout {
10000 const BORDER_WIDTH: Pixels = px(1.0);
10001 const LINE_MARKER_HEIGHT: Pixels = px(2.0);
10002 const MIN_MARKER_HEIGHT: Pixels = px(5.0);
10003 const MIN_THUMB_SIZE: Pixels = px(25.0);
10004
10005 fn new(
10006 scrollbar_track_hitbox: Hitbox,
10007 viewport_size: Pixels,
10008 scroll_range: Pixels,
10009 glyph_space: Pixels,
10010 content_offset: Pixels,
10011 scroll_position: ScrollOffset,
10012 show_thumb: bool,
10013 axis: ScrollbarAxis,
10014 ) -> Self {
10015 let track_bounds = scrollbar_track_hitbox.bounds;
10016 // The length of the track available to the scrollbar thumb. We deliberately
10017 // exclude the content size here so that the thumb aligns with the content.
10018 let track_length = track_bounds.size.along(axis) - content_offset;
10019
10020 Self::new_with_hitbox_and_track_length(
10021 scrollbar_track_hitbox,
10022 track_length,
10023 viewport_size,
10024 scroll_range.into(),
10025 glyph_space,
10026 content_offset.into(),
10027 scroll_position,
10028 show_thumb,
10029 axis,
10030 )
10031 }
10032
10033 fn for_minimap(
10034 minimap_track_hitbox: Hitbox,
10035 visible_lines: f64,
10036 total_editor_lines: f64,
10037 minimap_line_height: Pixels,
10038 scroll_position: ScrollOffset,
10039 minimap_scroll_top: ScrollOffset,
10040 show_thumb: bool,
10041 ) -> Self {
10042 // The scrollbar thumb size is calculated as
10043 // (visible_content/total_content) Γ scrollbar_track_length.
10044 //
10045 // For the minimap's thumb layout, we leverage this by setting the
10046 // scrollbar track length to the entire document size (using minimap line
10047 // height). This creates a thumb that exactly represents the editor
10048 // viewport scaled to minimap proportions.
10049 //
10050 // We adjust the thumb position relative to `minimap_scroll_top` to
10051 // accommodate for the deliberately oversized track.
10052 //
10053 // This approach ensures that the minimap thumb accurately reflects the
10054 // editor's current scroll position whilst nicely synchronizing the minimap
10055 // thumb and scrollbar thumb.
10056 let scroll_range = total_editor_lines * f64::from(minimap_line_height);
10057 let viewport_size = visible_lines * f64::from(minimap_line_height);
10058
10059 let track_top_offset = -minimap_scroll_top * f64::from(minimap_line_height);
10060
10061 Self::new_with_hitbox_and_track_length(
10062 minimap_track_hitbox,
10063 Pixels::from(scroll_range),
10064 Pixels::from(viewport_size),
10065 scroll_range,
10066 minimap_line_height,
10067 track_top_offset,
10068 scroll_position,
10069 show_thumb,
10070 ScrollbarAxis::Vertical,
10071 )
10072 }
10073
10074 fn new_with_hitbox_and_track_length(
10075 scrollbar_track_hitbox: Hitbox,
10076 track_length: Pixels,
10077 viewport_size: Pixels,
10078 scroll_range: f64,
10079 glyph_space: Pixels,
10080 content_offset: ScrollOffset,
10081 scroll_position: ScrollOffset,
10082 show_thumb: bool,
10083 axis: ScrollbarAxis,
10084 ) -> Self {
10085 let text_units_per_page = f64::from(viewport_size / glyph_space);
10086 let visible_range = scroll_position..scroll_position + text_units_per_page;
10087 let total_text_units = scroll_range / f64::from(glyph_space);
10088
10089 let thumb_percentage = text_units_per_page / total_text_units;
10090 let thumb_size = Pixels::from(ScrollOffset::from(track_length) * thumb_percentage)
10091 .max(ScrollbarLayout::MIN_THUMB_SIZE)
10092 .min(track_length);
10093
10094 let text_unit_divisor = (total_text_units - text_units_per_page).max(0.);
10095
10096 let content_larger_than_viewport = text_unit_divisor > 0.;
10097
10098 let text_unit_size = if content_larger_than_viewport {
10099 Pixels::from(ScrollOffset::from(track_length - thumb_size) / text_unit_divisor)
10100 } else {
10101 glyph_space
10102 };
10103
10104 let thumb_bounds = (show_thumb && content_larger_than_viewport).then(|| {
10105 Self::thumb_bounds(
10106 &scrollbar_track_hitbox,
10107 content_offset,
10108 visible_range.start,
10109 text_unit_size,
10110 thumb_size,
10111 axis,
10112 )
10113 });
10114
10115 ScrollbarLayout {
10116 hitbox: scrollbar_track_hitbox,
10117 visible_range,
10118 text_unit_size,
10119 thumb_bounds,
10120 thumb_state: Default::default(),
10121 }
10122 }
10123
10124 fn with_thumb_state(self, thumb_state: Option<ScrollbarThumbState>) -> Self {
10125 if let Some(thumb_state) = thumb_state {
10126 Self {
10127 thumb_state,
10128 ..self
10129 }
10130 } else {
10131 self
10132 }
10133 }
10134
10135 fn thumb_bounds(
10136 scrollbar_track: &Hitbox,
10137 content_offset: f64,
10138 visible_range_start: f64,
10139 text_unit_size: Pixels,
10140 thumb_size: Pixels,
10141 axis: ScrollbarAxis,
10142 ) -> Bounds<Pixels> {
10143 let thumb_origin = scrollbar_track.origin.apply_along(axis, |origin| {
10144 origin
10145 + Pixels::from(
10146 content_offset + visible_range_start * ScrollOffset::from(text_unit_size),
10147 )
10148 });
10149 Bounds::new(
10150 thumb_origin,
10151 scrollbar_track.size.apply_along(axis, |_| thumb_size),
10152 )
10153 }
10154
10155 fn thumb_hovered(&self, position: &gpui::Point<Pixels>) -> bool {
10156 self.thumb_bounds
10157 .is_some_and(|bounds| bounds.contains(position))
10158 }
10159
10160 fn marker_quads_for_ranges(
10161 &self,
10162 row_ranges: impl IntoIterator<Item = ColoredRange<DisplayRow>>,
10163 column: Option<usize>,
10164 ) -> Vec<PaintQuad> {
10165 struct MinMax {
10166 min: Pixels,
10167 max: Pixels,
10168 }
10169 let (x_range, height_limit) = if let Some(column) = column {
10170 let column_width = ((self.hitbox.size.width - Self::BORDER_WIDTH) / 3.0).floor();
10171 let start = Self::BORDER_WIDTH + (column as f32 * column_width);
10172 let end = start + column_width;
10173 (
10174 Range { start, end },
10175 MinMax {
10176 min: Self::MIN_MARKER_HEIGHT,
10177 max: px(f32::MAX),
10178 },
10179 )
10180 } else {
10181 (
10182 Range {
10183 start: Self::BORDER_WIDTH,
10184 end: self.hitbox.size.width,
10185 },
10186 MinMax {
10187 min: Self::LINE_MARKER_HEIGHT,
10188 max: Self::LINE_MARKER_HEIGHT,
10189 },
10190 )
10191 };
10192
10193 let row_to_y = |row: DisplayRow| row.as_f64() as f32 * self.text_unit_size;
10194 let mut pixel_ranges = row_ranges
10195 .into_iter()
10196 .map(|range| {
10197 let start_y = row_to_y(range.start);
10198 let end_y = row_to_y(range.end)
10199 + self
10200 .text_unit_size
10201 .max(height_limit.min)
10202 .min(height_limit.max);
10203 ColoredRange {
10204 start: start_y,
10205 end: end_y,
10206 color: range.color,
10207 }
10208 })
10209 .peekable();
10210
10211 let mut quads = Vec::new();
10212 while let Some(mut pixel_range) = pixel_ranges.next() {
10213 while let Some(next_pixel_range) = pixel_ranges.peek() {
10214 if pixel_range.end >= next_pixel_range.start - px(1.0)
10215 && pixel_range.color == next_pixel_range.color
10216 {
10217 pixel_range.end = next_pixel_range.end.max(pixel_range.end);
10218 pixel_ranges.next();
10219 } else {
10220 break;
10221 }
10222 }
10223
10224 let bounds = Bounds::from_corners(
10225 point(x_range.start, pixel_range.start),
10226 point(x_range.end, pixel_range.end),
10227 );
10228 quads.push(quad(
10229 bounds,
10230 Corners::default(),
10231 pixel_range.color,
10232 Edges::default(),
10233 Hsla::transparent_black(),
10234 BorderStyle::default(),
10235 ));
10236 }
10237
10238 quads
10239 }
10240}
10241
10242struct MinimapLayout {
10243 pub minimap: AnyElement,
10244 pub thumb_layout: ScrollbarLayout,
10245 pub minimap_scroll_top: ScrollOffset,
10246 pub minimap_line_height: Pixels,
10247 pub thumb_border_style: MinimapThumbBorder,
10248 pub max_scroll_top: ScrollOffset,
10249}
10250
10251impl MinimapLayout {
10252 /// The minimum width of the minimap in columns. If the minimap is smaller than this, it will be hidden.
10253 const MINIMAP_MIN_WIDTH_COLUMNS: f32 = 20.;
10254 /// The minimap width as a percentage of the editor width.
10255 const MINIMAP_WIDTH_PCT: f32 = 0.15;
10256 /// Calculates the scroll top offset the minimap editor has to have based on the
10257 /// current scroll progress.
10258 fn calculate_minimap_top_offset(
10259 document_lines: f64,
10260 visible_editor_lines: f64,
10261 visible_minimap_lines: f64,
10262 scroll_position: f64,
10263 ) -> ScrollOffset {
10264 let non_visible_document_lines = (document_lines - visible_editor_lines).max(0.);
10265 if non_visible_document_lines == 0. {
10266 0.
10267 } else {
10268 let scroll_percentage = (scroll_position / non_visible_document_lines).clamp(0., 1.);
10269 scroll_percentage * (document_lines - visible_minimap_lines).max(0.)
10270 }
10271 }
10272}
10273
10274struct CreaseTrailerLayout {
10275 element: AnyElement,
10276 bounds: Bounds<Pixels>,
10277}
10278
10279pub(crate) struct PositionMap {
10280 pub size: Size<Pixels>,
10281 pub line_height: Pixels,
10282 pub scroll_position: gpui::Point<ScrollOffset>,
10283 pub scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
10284 pub scroll_max: gpui::Point<ScrollOffset>,
10285 pub em_width: Pixels,
10286 pub em_advance: Pixels,
10287 pub visible_row_range: Range<DisplayRow>,
10288 pub line_layouts: Vec<LineWithInvisibles>,
10289 pub snapshot: EditorSnapshot,
10290 pub text_hitbox: Hitbox,
10291 pub gutter_hitbox: Hitbox,
10292 pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,
10293 pub display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
10294 pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds<Pixels>)>,
10295}
10296
10297#[derive(Debug, Copy, Clone)]
10298pub struct PointForPosition {
10299 pub previous_valid: DisplayPoint,
10300 pub next_valid: DisplayPoint,
10301 pub exact_unclipped: DisplayPoint,
10302 pub column_overshoot_after_line_end: u32,
10303}
10304
10305impl PointForPosition {
10306 pub fn as_valid(&self) -> Option<DisplayPoint> {
10307 if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
10308 Some(self.previous_valid)
10309 } else {
10310 None
10311 }
10312 }
10313
10314 pub fn intersects_selection(&self, selection: &Selection<DisplayPoint>) -> bool {
10315 let Some(valid_point) = self.as_valid() else {
10316 return false;
10317 };
10318 let range = selection.range();
10319
10320 let candidate_row = valid_point.row();
10321 let candidate_col = valid_point.column();
10322
10323 let start_row = range.start.row();
10324 let start_col = range.start.column();
10325 let end_row = range.end.row();
10326 let end_col = range.end.column();
10327
10328 if candidate_row < start_row || candidate_row > end_row {
10329 false
10330 } else if start_row == end_row {
10331 candidate_col >= start_col && candidate_col < end_col
10332 } else if candidate_row == start_row {
10333 candidate_col >= start_col
10334 } else if candidate_row == end_row {
10335 candidate_col < end_col
10336 } else {
10337 true
10338 }
10339 }
10340}
10341
10342impl PositionMap {
10343 pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
10344 let text_bounds = self.text_hitbox.bounds;
10345 let scroll_position = self.snapshot.scroll_position();
10346 let position = position - text_bounds.origin;
10347 let y = position.y.max(px(0.)).min(self.size.height);
10348 let x = position.x + (scroll_position.x as f32 * self.em_advance);
10349 let row = ((y / self.line_height) as f64 + scroll_position.y) as u32;
10350
10351 let (column, x_overshoot_after_line_end) = if let Some(line) = self
10352 .line_layouts
10353 .get(row as usize - scroll_position.y as usize)
10354 {
10355 if let Some(ix) = line.index_for_x(x) {
10356 (ix as u32, px(0.))
10357 } else {
10358 (line.len as u32, px(0.).max(x - line.width))
10359 }
10360 } else {
10361 (0, x)
10362 };
10363
10364 let mut exact_unclipped = DisplayPoint::new(DisplayRow(row), column);
10365 let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
10366 let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
10367
10368 let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
10369 *exact_unclipped.column_mut() += column_overshoot_after_line_end;
10370 PointForPosition {
10371 previous_valid,
10372 next_valid,
10373 exact_unclipped,
10374 column_overshoot_after_line_end,
10375 }
10376 }
10377}
10378
10379struct BlockLayout {
10380 id: BlockId,
10381 x_offset: Pixels,
10382 row: Option<DisplayRow>,
10383 element: AnyElement,
10384 available_space: Size<AvailableSpace>,
10385 style: BlockStyle,
10386 overlaps_gutter: bool,
10387 is_buffer_header: bool,
10388}
10389
10390pub fn layout_line(
10391 row: DisplayRow,
10392 snapshot: &EditorSnapshot,
10393 style: &EditorStyle,
10394 text_width: Pixels,
10395 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
10396 window: &mut Window,
10397 cx: &mut App,
10398) -> LineWithInvisibles {
10399 let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
10400 LineWithInvisibles::from_chunks(
10401 chunks,
10402 style,
10403 MAX_LINE_LEN,
10404 1,
10405 &snapshot.mode,
10406 text_width,
10407 is_row_soft_wrapped,
10408 &[],
10409 window,
10410 cx,
10411 )
10412 .pop()
10413 .unwrap()
10414}
10415
10416#[derive(Debug)]
10417pub struct IndentGuideLayout {
10418 origin: gpui::Point<Pixels>,
10419 length: Pixels,
10420 single_indent_width: Pixels,
10421 depth: u32,
10422 active: bool,
10423 settings: IndentGuideSettings,
10424}
10425
10426pub struct CursorLayout {
10427 origin: gpui::Point<Pixels>,
10428 block_width: Pixels,
10429 line_height: Pixels,
10430 color: Hsla,
10431 shape: CursorShape,
10432 block_text: Option<ShapedLine>,
10433 cursor_name: Option<AnyElement>,
10434}
10435
10436#[derive(Debug)]
10437pub struct CursorName {
10438 string: SharedString,
10439 color: Hsla,
10440 is_top_row: bool,
10441}
10442
10443impl CursorLayout {
10444 pub fn new(
10445 origin: gpui::Point<Pixels>,
10446 block_width: Pixels,
10447 line_height: Pixels,
10448 color: Hsla,
10449 shape: CursorShape,
10450 block_text: Option<ShapedLine>,
10451 ) -> CursorLayout {
10452 CursorLayout {
10453 origin,
10454 block_width,
10455 line_height,
10456 color,
10457 shape,
10458 block_text,
10459 cursor_name: None,
10460 }
10461 }
10462
10463 pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
10464 Bounds {
10465 origin: self.origin + origin,
10466 size: size(self.block_width, self.line_height),
10467 }
10468 }
10469
10470 fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
10471 match self.shape {
10472 CursorShape::Bar => Bounds {
10473 origin: self.origin + origin,
10474 size: size(px(2.0), self.line_height),
10475 },
10476 CursorShape::Block | CursorShape::Hollow => Bounds {
10477 origin: self.origin + origin,
10478 size: size(self.block_width, self.line_height),
10479 },
10480 CursorShape::Underline => Bounds {
10481 origin: self.origin
10482 + origin
10483 + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)),
10484 size: size(self.block_width, px(2.0)),
10485 },
10486 }
10487 }
10488
10489 pub fn layout(
10490 &mut self,
10491 origin: gpui::Point<Pixels>,
10492 cursor_name: Option<CursorName>,
10493 window: &mut Window,
10494 cx: &mut App,
10495 ) {
10496 if let Some(cursor_name) = cursor_name {
10497 let bounds = self.bounds(origin);
10498 let text_size = self.line_height / 1.5;
10499
10500 let name_origin = if cursor_name.is_top_row {
10501 point(bounds.right() - px(1.), bounds.top())
10502 } else {
10503 match self.shape {
10504 CursorShape::Bar => point(
10505 bounds.right() - px(2.),
10506 bounds.top() - text_size / 2. - px(1.),
10507 ),
10508 _ => point(
10509 bounds.right() - px(1.),
10510 bounds.top() - text_size / 2. - px(1.),
10511 ),
10512 }
10513 };
10514 let mut name_element = div()
10515 .bg(self.color)
10516 .text_size(text_size)
10517 .px_0p5()
10518 .line_height(text_size + px(2.))
10519 .text_color(cursor_name.color)
10520 .child(cursor_name.string)
10521 .into_any_element();
10522
10523 name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx);
10524
10525 self.cursor_name = Some(name_element);
10526 }
10527 }
10528
10529 pub fn paint(&mut self, origin: gpui::Point<Pixels>, window: &mut Window, cx: &mut App) {
10530 let bounds = self.bounds(origin);
10531
10532 //Draw background or border quad
10533 let cursor = if matches!(self.shape, CursorShape::Hollow) {
10534 outline(bounds, self.color, BorderStyle::Solid)
10535 } else {
10536 fill(bounds, self.color)
10537 };
10538
10539 if let Some(name) = &mut self.cursor_name {
10540 name.paint(window, cx);
10541 }
10542
10543 window.paint_quad(cursor);
10544
10545 if let Some(block_text) = &self.block_text {
10546 block_text
10547 .paint(self.origin + origin, self.line_height, window, cx)
10548 .log_err();
10549 }
10550 }
10551
10552 pub fn shape(&self) -> CursorShape {
10553 self.shape
10554 }
10555}
10556
10557#[derive(Debug)]
10558pub struct HighlightedRange {
10559 pub start_y: Pixels,
10560 pub line_height: Pixels,
10561 pub lines: Vec<HighlightedRangeLine>,
10562 pub color: Hsla,
10563 pub corner_radius: Pixels,
10564}
10565
10566#[derive(Debug)]
10567pub struct HighlightedRangeLine {
10568 pub start_x: Pixels,
10569 pub end_x: Pixels,
10570}
10571
10572impl HighlightedRange {
10573 pub fn paint(&self, fill: bool, bounds: Bounds<Pixels>, window: &mut Window) {
10574 if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
10575 self.paint_lines(self.start_y, &self.lines[0..1], fill, bounds, window);
10576 self.paint_lines(
10577 self.start_y + self.line_height,
10578 &self.lines[1..],
10579 fill,
10580 bounds,
10581 window,
10582 );
10583 } else {
10584 self.paint_lines(self.start_y, &self.lines, fill, bounds, window);
10585 }
10586 }
10587
10588 fn paint_lines(
10589 &self,
10590 start_y: Pixels,
10591 lines: &[HighlightedRangeLine],
10592 fill: bool,
10593 _bounds: Bounds<Pixels>,
10594 window: &mut Window,
10595 ) {
10596 if lines.is_empty() {
10597 return;
10598 }
10599
10600 let first_line = lines.first().unwrap();
10601 let last_line = lines.last().unwrap();
10602
10603 let first_top_left = point(first_line.start_x, start_y);
10604 let first_top_right = point(first_line.end_x, start_y);
10605
10606 let curve_height = point(Pixels::ZERO, self.corner_radius);
10607 let curve_width = |start_x: Pixels, end_x: Pixels| {
10608 let max = (end_x - start_x) / 2.;
10609 let width = if max < self.corner_radius {
10610 max
10611 } else {
10612 self.corner_radius
10613 };
10614
10615 point(width, Pixels::ZERO)
10616 };
10617
10618 let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
10619 let mut builder = if fill {
10620 gpui::PathBuilder::fill()
10621 } else {
10622 gpui::PathBuilder::stroke(px(1.))
10623 };
10624 builder.move_to(first_top_right - top_curve_width);
10625 builder.curve_to(first_top_right + curve_height, first_top_right);
10626
10627 let mut iter = lines.iter().enumerate().peekable();
10628 while let Some((ix, line)) = iter.next() {
10629 let bottom_right = point(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
10630
10631 if let Some((_, next_line)) = iter.peek() {
10632 let next_top_right = point(next_line.end_x, bottom_right.y);
10633
10634 match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() {
10635 Ordering::Equal => {
10636 builder.line_to(bottom_right);
10637 }
10638 Ordering::Less => {
10639 let curve_width = curve_width(next_top_right.x, bottom_right.x);
10640 builder.line_to(bottom_right - curve_height);
10641 if self.corner_radius > Pixels::ZERO {
10642 builder.curve_to(bottom_right - curve_width, bottom_right);
10643 }
10644 builder.line_to(next_top_right + curve_width);
10645 if self.corner_radius > Pixels::ZERO {
10646 builder.curve_to(next_top_right + curve_height, next_top_right);
10647 }
10648 }
10649 Ordering::Greater => {
10650 let curve_width = curve_width(bottom_right.x, next_top_right.x);
10651 builder.line_to(bottom_right - curve_height);
10652 if self.corner_radius > Pixels::ZERO {
10653 builder.curve_to(bottom_right + curve_width, bottom_right);
10654 }
10655 builder.line_to(next_top_right - curve_width);
10656 if self.corner_radius > Pixels::ZERO {
10657 builder.curve_to(next_top_right + curve_height, next_top_right);
10658 }
10659 }
10660 }
10661 } else {
10662 let curve_width = curve_width(line.start_x, line.end_x);
10663 builder.line_to(bottom_right - curve_height);
10664 if self.corner_radius > Pixels::ZERO {
10665 builder.curve_to(bottom_right - curve_width, bottom_right);
10666 }
10667
10668 let bottom_left = point(line.start_x, bottom_right.y);
10669 builder.line_to(bottom_left + curve_width);
10670 if self.corner_radius > Pixels::ZERO {
10671 builder.curve_to(bottom_left - curve_height, bottom_left);
10672 }
10673 }
10674 }
10675
10676 if first_line.start_x > last_line.start_x {
10677 let curve_width = curve_width(last_line.start_x, first_line.start_x);
10678 let second_top_left = point(last_line.start_x, start_y + self.line_height);
10679 builder.line_to(second_top_left + curve_height);
10680 if self.corner_radius > Pixels::ZERO {
10681 builder.curve_to(second_top_left + curve_width, second_top_left);
10682 }
10683 let first_bottom_left = point(first_line.start_x, second_top_left.y);
10684 builder.line_to(first_bottom_left - curve_width);
10685 if self.corner_radius > Pixels::ZERO {
10686 builder.curve_to(first_bottom_left - curve_height, first_bottom_left);
10687 }
10688 }
10689
10690 builder.line_to(first_top_left + curve_height);
10691 if self.corner_radius > Pixels::ZERO {
10692 builder.curve_to(first_top_left + top_curve_width, first_top_left);
10693 }
10694 builder.line_to(first_top_right - top_curve_width);
10695
10696 if let Ok(path) = builder.build() {
10697 window.paint_path(path, self.color);
10698 }
10699 }
10700}
10701
10702enum CursorPopoverType {
10703 CodeContextMenu,
10704 EditPrediction,
10705}
10706
10707pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
10708 (delta.pow(1.2) / 100.0).min(px(3.0)).into()
10709}
10710
10711fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
10712 (delta.pow(1.2) / 300.0).into()
10713}
10714
10715pub fn register_action<T: Action>(
10716 editor: &Entity<Editor>,
10717 window: &mut Window,
10718 listener: impl Fn(&mut Editor, &T, &mut Window, &mut Context<Editor>) + 'static,
10719) {
10720 let editor = editor.clone();
10721 window.on_action(TypeId::of::<T>(), move |action, phase, window, cx| {
10722 let action = action.downcast_ref().unwrap();
10723 if phase == DispatchPhase::Bubble {
10724 editor.update(cx, |editor, cx| {
10725 listener(editor, action, window, cx);
10726 })
10727 }
10728 })
10729}
10730
10731fn compute_auto_height_layout(
10732 editor: &mut Editor,
10733 min_lines: usize,
10734 max_lines: Option<usize>,
10735 max_line_number_width: Pixels,
10736 known_dimensions: Size<Option<Pixels>>,
10737 available_width: AvailableSpace,
10738 window: &mut Window,
10739 cx: &mut Context<Editor>,
10740) -> Option<Size<Pixels>> {
10741 let width = known_dimensions.width.or({
10742 if let AvailableSpace::Definite(available_width) = available_width {
10743 Some(available_width)
10744 } else {
10745 None
10746 }
10747 })?;
10748 if let Some(height) = known_dimensions.height {
10749 return Some(size(width, height));
10750 }
10751
10752 let style = editor.style.as_ref().unwrap();
10753 let font_id = window.text_system().resolve_font(&style.text.font());
10754 let font_size = style.text.font_size.to_pixels(window.rem_size());
10755 let line_height = style.text.line_height_in_pixels(window.rem_size());
10756 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
10757
10758 let mut snapshot = editor.snapshot(window, cx);
10759 let gutter_dimensions = snapshot
10760 .gutter_dimensions(font_id, font_size, max_line_number_width, cx)
10761 .or_else(|| {
10762 editor
10763 .offset_content
10764 .then(|| GutterDimensions::default_with_margin(font_id, font_size, cx))
10765 })
10766 .unwrap_or_default();
10767
10768 editor.gutter_dimensions = gutter_dimensions;
10769 let text_width = width - gutter_dimensions.width;
10770 let overscroll = size(em_width, px(0.));
10771
10772 let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
10773 if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None)
10774 && editor.set_wrap_width(Some(editor_width), cx)
10775 {
10776 snapshot = editor.snapshot(window, cx);
10777 }
10778
10779 let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
10780
10781 let min_height = line_height * min_lines as f32;
10782 let content_height = scroll_height.max(min_height);
10783
10784 let final_height = if let Some(max_lines) = max_lines {
10785 let max_height = line_height * max_lines as f32;
10786 content_height.min(max_height)
10787 } else {
10788 content_height
10789 };
10790
10791 Some(size(width, final_height))
10792}
10793
10794#[cfg(test)]
10795mod tests {
10796 use super::*;
10797 use crate::{
10798 Editor, MultiBuffer, SelectionEffects,
10799 display_map::{BlockPlacement, BlockProperties},
10800 editor_tests::{init_test, update_test_language_settings},
10801 };
10802 use gpui::{TestAppContext, VisualTestContext};
10803 use language::language_settings;
10804 use log::info;
10805 use std::num::NonZeroU32;
10806 use util::test::sample_text;
10807
10808 #[gpui::test]
10809 async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) {
10810 init_test(cx, |_| {});
10811
10812 let window = cx.add_window(|window, cx| {
10813 let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
10814 let mut editor = Editor::new(
10815 EditorMode::AutoHeight {
10816 min_lines: 1,
10817 max_lines: None,
10818 },
10819 buffer,
10820 None,
10821 window,
10822 cx,
10823 );
10824 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
10825 editor
10826 });
10827 let cx = &mut VisualTestContext::from_window(*window, cx);
10828 let editor = window.root(cx).unwrap();
10829 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
10830
10831 for x in 1..=100 {
10832 let (_, state) = cx.draw(
10833 Default::default(),
10834 size(px(200. + 0.13 * x as f32), px(500.)),
10835 |_, _| EditorElement::new(&editor, style.clone()),
10836 );
10837
10838 assert!(
10839 state.position_map.scroll_max.x == 0.,
10840 "Soft wrapped editor should have no horizontal scrolling!"
10841 );
10842 }
10843 }
10844
10845 #[gpui::test]
10846 async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) {
10847 init_test(cx, |_| {});
10848
10849 let window = cx.add_window(|window, cx| {
10850 let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
10851 let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
10852 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
10853 editor
10854 });
10855 let cx = &mut VisualTestContext::from_window(*window, cx);
10856 let editor = window.root(cx).unwrap();
10857 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
10858
10859 for x in 1..=100 {
10860 let (_, state) = cx.draw(
10861 Default::default(),
10862 size(px(200. + 0.13 * x as f32), px(500.)),
10863 |_, _| EditorElement::new(&editor, style.clone()),
10864 );
10865
10866 assert!(
10867 state.position_map.scroll_max.x == 0.,
10868 "Soft wrapped editor should have no horizontal scrolling!"
10869 );
10870 }
10871 }
10872
10873 #[gpui::test]
10874 fn test_shape_line_numbers(cx: &mut TestAppContext) {
10875 init_test(cx, |_| {});
10876 let window = cx.add_window(|window, cx| {
10877 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
10878 Editor::new(EditorMode::full(), buffer, None, window, cx)
10879 });
10880
10881 let editor = window.root(cx).unwrap();
10882 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
10883 let line_height = window
10884 .update(cx, |_, window, _| {
10885 style.text.line_height_in_pixels(window.rem_size())
10886 })
10887 .unwrap();
10888 let element = EditorElement::new(&editor, style);
10889 let snapshot = window
10890 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
10891 .unwrap();
10892
10893 let layouts = cx
10894 .update_window(*window, |_, window, cx| {
10895 element.layout_line_numbers(
10896 None,
10897 GutterDimensions {
10898 left_padding: Pixels::ZERO,
10899 right_padding: Pixels::ZERO,
10900 width: px(30.0),
10901 margin: Pixels::ZERO,
10902 git_blame_entries_width: None,
10903 },
10904 line_height,
10905 gpui::Point::default(),
10906 DisplayRow(0)..DisplayRow(6),
10907 &(0..6)
10908 .map(|row| RowInfo {
10909 buffer_row: Some(row),
10910 ..Default::default()
10911 })
10912 .collect::<Vec<_>>(),
10913 &BTreeMap::default(),
10914 Some(DisplayPoint::new(DisplayRow(0), 0)),
10915 &snapshot,
10916 window,
10917 cx,
10918 )
10919 })
10920 .unwrap();
10921 assert_eq!(layouts.len(), 6);
10922
10923 let relative_rows = window
10924 .update(cx, |editor, window, cx| {
10925 let snapshot = editor.snapshot(window, cx);
10926 element.calculate_relative_line_numbers(
10927 &snapshot,
10928 &(DisplayRow(0)..DisplayRow(6)),
10929 Some(DisplayRow(3)),
10930 false,
10931 )
10932 })
10933 .unwrap();
10934 assert_eq!(relative_rows[&DisplayRow(0)], 3);
10935 assert_eq!(relative_rows[&DisplayRow(1)], 2);
10936 assert_eq!(relative_rows[&DisplayRow(2)], 1);
10937 // current line has no relative number
10938 assert_eq!(relative_rows[&DisplayRow(4)], 1);
10939 assert_eq!(relative_rows[&DisplayRow(5)], 2);
10940
10941 // works if cursor is before screen
10942 let relative_rows = window
10943 .update(cx, |editor, window, cx| {
10944 let snapshot = editor.snapshot(window, cx);
10945 element.calculate_relative_line_numbers(
10946 &snapshot,
10947 &(DisplayRow(3)..DisplayRow(6)),
10948 Some(DisplayRow(1)),
10949 false,
10950 )
10951 })
10952 .unwrap();
10953 assert_eq!(relative_rows.len(), 3);
10954 assert_eq!(relative_rows[&DisplayRow(3)], 2);
10955 assert_eq!(relative_rows[&DisplayRow(4)], 3);
10956 assert_eq!(relative_rows[&DisplayRow(5)], 4);
10957
10958 // works if cursor is after screen
10959 let relative_rows = window
10960 .update(cx, |editor, window, cx| {
10961 let snapshot = editor.snapshot(window, cx);
10962 element.calculate_relative_line_numbers(
10963 &snapshot,
10964 &(DisplayRow(0)..DisplayRow(3)),
10965 Some(DisplayRow(6)),
10966 false,
10967 )
10968 })
10969 .unwrap();
10970 assert_eq!(relative_rows.len(), 3);
10971 assert_eq!(relative_rows[&DisplayRow(0)], 5);
10972 assert_eq!(relative_rows[&DisplayRow(1)], 4);
10973 assert_eq!(relative_rows[&DisplayRow(2)], 3);
10974 }
10975
10976 #[gpui::test]
10977 fn test_shape_line_numbers_wrapping(cx: &mut TestAppContext) {
10978 init_test(cx, |_| {});
10979 let window = cx.add_window(|window, cx| {
10980 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
10981 Editor::new(EditorMode::full(), buffer, None, window, cx)
10982 });
10983
10984 update_test_language_settings(cx, |s| {
10985 s.defaults.preferred_line_length = Some(5_u32);
10986 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
10987 });
10988
10989 let editor = window.root(cx).unwrap();
10990 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
10991 let line_height = window
10992 .update(cx, |_, window, _| {
10993 style.text.line_height_in_pixels(window.rem_size())
10994 })
10995 .unwrap();
10996 let element = EditorElement::new(&editor, style);
10997 let snapshot = window
10998 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
10999 .unwrap();
11000
11001 let layouts = cx
11002 .update_window(*window, |_, window, cx| {
11003 element.layout_line_numbers(
11004 None,
11005 GutterDimensions {
11006 left_padding: Pixels::ZERO,
11007 right_padding: Pixels::ZERO,
11008 width: px(30.0),
11009 margin: Pixels::ZERO,
11010 git_blame_entries_width: None,
11011 },
11012 line_height,
11013 gpui::Point::default(),
11014 DisplayRow(0)..DisplayRow(6),
11015 &(0..6)
11016 .map(|row| RowInfo {
11017 buffer_row: Some(row),
11018 ..Default::default()
11019 })
11020 .collect::<Vec<_>>(),
11021 &BTreeMap::default(),
11022 Some(DisplayPoint::new(DisplayRow(0), 0)),
11023 &snapshot,
11024 window,
11025 cx,
11026 )
11027 })
11028 .unwrap();
11029 assert_eq!(layouts.len(), 3);
11030
11031 let relative_rows = window
11032 .update(cx, |editor, window, cx| {
11033 let snapshot = editor.snapshot(window, cx);
11034 element.calculate_relative_line_numbers(
11035 &snapshot,
11036 &(DisplayRow(0)..DisplayRow(6)),
11037 Some(DisplayRow(3)),
11038 true,
11039 )
11040 })
11041 .unwrap();
11042
11043 assert_eq!(relative_rows[&DisplayRow(0)], 3);
11044 assert_eq!(relative_rows[&DisplayRow(1)], 2);
11045 assert_eq!(relative_rows[&DisplayRow(2)], 1);
11046 // current line has no relative number
11047 assert_eq!(relative_rows[&DisplayRow(4)], 1);
11048 assert_eq!(relative_rows[&DisplayRow(5)], 2);
11049 }
11050
11051 #[gpui::test]
11052 async fn test_vim_visual_selections(cx: &mut TestAppContext) {
11053 init_test(cx, |_| {});
11054
11055 let window = cx.add_window(|window, cx| {
11056 let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
11057 Editor::new(EditorMode::full(), buffer, None, window, cx)
11058 });
11059 let cx = &mut VisualTestContext::from_window(*window, cx);
11060 let editor = window.root(cx).unwrap();
11061 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
11062
11063 window
11064 .update(cx, |editor, window, cx| {
11065 editor.cursor_shape = CursorShape::Block;
11066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11067 s.select_ranges([
11068 Point::new(0, 0)..Point::new(1, 0),
11069 Point::new(3, 2)..Point::new(3, 3),
11070 Point::new(5, 6)..Point::new(6, 0),
11071 ]);
11072 });
11073 })
11074 .unwrap();
11075
11076 let (_, state) = cx.draw(
11077 point(px(500.), px(500.)),
11078 size(px(500.), px(500.)),
11079 |_, _| EditorElement::new(&editor, style),
11080 );
11081
11082 assert_eq!(state.selections.len(), 1);
11083 let local_selections = &state.selections[0].1;
11084 assert_eq!(local_selections.len(), 3);
11085 // moves cursor back one line
11086 assert_eq!(
11087 local_selections[0].head,
11088 DisplayPoint::new(DisplayRow(0), 6)
11089 );
11090 assert_eq!(
11091 local_selections[0].range,
11092 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0)
11093 );
11094
11095 // moves cursor back one column
11096 assert_eq!(
11097 local_selections[1].range,
11098 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 3)
11099 );
11100 assert_eq!(
11101 local_selections[1].head,
11102 DisplayPoint::new(DisplayRow(3), 2)
11103 );
11104
11105 // leaves cursor on the max point
11106 assert_eq!(
11107 local_selections[2].range,
11108 DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(6), 0)
11109 );
11110 assert_eq!(
11111 local_selections[2].head,
11112 DisplayPoint::new(DisplayRow(6), 0)
11113 );
11114
11115 // active lines does not include 1 (even though the range of the selection does)
11116 assert_eq!(
11117 state.active_rows.keys().cloned().collect::<Vec<_>>(),
11118 vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
11119 );
11120 }
11121
11122 #[gpui::test]
11123 fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
11124 init_test(cx, |_| {});
11125
11126 let window = cx.add_window(|window, cx| {
11127 let buffer = MultiBuffer::build_simple("", cx);
11128 Editor::new(EditorMode::full(), buffer, None, window, cx)
11129 });
11130 let cx = &mut VisualTestContext::from_window(*window, cx);
11131 let editor = window.root(cx).unwrap();
11132 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
11133 window
11134 .update(cx, |editor, window, cx| {
11135 editor.set_placeholder_text("hello", window, cx);
11136 editor.insert_blocks(
11137 [BlockProperties {
11138 style: BlockStyle::Fixed,
11139 placement: BlockPlacement::Above(Anchor::min()),
11140 height: Some(3),
11141 render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
11142 priority: 0,
11143 }],
11144 None,
11145 cx,
11146 );
11147
11148 // Blur the editor so that it displays placeholder text.
11149 window.blur();
11150 })
11151 .unwrap();
11152
11153 let (_, state) = cx.draw(
11154 point(px(500.), px(500.)),
11155 size(px(500.), px(500.)),
11156 |_, _| EditorElement::new(&editor, style),
11157 );
11158 assert_eq!(state.position_map.line_layouts.len(), 4);
11159 assert_eq!(state.line_numbers.len(), 1);
11160 assert_eq!(
11161 state
11162 .line_numbers
11163 .get(&MultiBufferRow(0))
11164 .map(|line_number| line_number
11165 .segments
11166 .first()
11167 .unwrap()
11168 .shaped_line
11169 .text
11170 .as_ref()),
11171 Some("1")
11172 );
11173 }
11174
11175 #[gpui::test]
11176 fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
11177 const TAB_SIZE: u32 = 4;
11178
11179 let input_text = "\t \t|\t| a b";
11180 let expected_invisibles = vec![
11181 Invisible::Tab {
11182 line_start_offset: 0,
11183 line_end_offset: TAB_SIZE as usize,
11184 },
11185 Invisible::Whitespace {
11186 line_offset: TAB_SIZE as usize,
11187 },
11188 Invisible::Tab {
11189 line_start_offset: TAB_SIZE as usize + 1,
11190 line_end_offset: TAB_SIZE as usize * 2,
11191 },
11192 Invisible::Tab {
11193 line_start_offset: TAB_SIZE as usize * 2 + 1,
11194 line_end_offset: TAB_SIZE as usize * 3,
11195 },
11196 Invisible::Whitespace {
11197 line_offset: TAB_SIZE as usize * 3 + 1,
11198 },
11199 Invisible::Whitespace {
11200 line_offset: TAB_SIZE as usize * 3 + 3,
11201 },
11202 ];
11203 assert_eq!(
11204 expected_invisibles.len(),
11205 input_text
11206 .chars()
11207 .filter(|initial_char| initial_char.is_whitespace())
11208 .count(),
11209 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
11210 );
11211
11212 for show_line_numbers in [true, false] {
11213 init_test(cx, |s| {
11214 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
11215 s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
11216 });
11217
11218 let actual_invisibles = collect_invisibles_from_new_editor(
11219 cx,
11220 EditorMode::full(),
11221 input_text,
11222 px(500.0),
11223 show_line_numbers,
11224 );
11225
11226 assert_eq!(expected_invisibles, actual_invisibles);
11227 }
11228 }
11229
11230 #[gpui::test]
11231 fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
11232 init_test(cx, |s| {
11233 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
11234 s.defaults.tab_size = NonZeroU32::new(4);
11235 });
11236
11237 for editor_mode_without_invisibles in [
11238 EditorMode::SingleLine,
11239 EditorMode::AutoHeight {
11240 min_lines: 1,
11241 max_lines: Some(100),
11242 },
11243 ] {
11244 for show_line_numbers in [true, false] {
11245 let invisibles = collect_invisibles_from_new_editor(
11246 cx,
11247 editor_mode_without_invisibles.clone(),
11248 "\t\t\t| | a b",
11249 px(500.0),
11250 show_line_numbers,
11251 );
11252 assert!(
11253 invisibles.is_empty(),
11254 "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"
11255 );
11256 }
11257 }
11258 }
11259
11260 #[gpui::test]
11261 fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
11262 let tab_size = 4;
11263 let input_text = "a\tbcd ".repeat(9);
11264 let repeated_invisibles = [
11265 Invisible::Tab {
11266 line_start_offset: 1,
11267 line_end_offset: tab_size as usize,
11268 },
11269 Invisible::Whitespace {
11270 line_offset: tab_size as usize + 3,
11271 },
11272 Invisible::Whitespace {
11273 line_offset: tab_size as usize + 4,
11274 },
11275 Invisible::Whitespace {
11276 line_offset: tab_size as usize + 5,
11277 },
11278 Invisible::Whitespace {
11279 line_offset: tab_size as usize + 6,
11280 },
11281 Invisible::Whitespace {
11282 line_offset: tab_size as usize + 7,
11283 },
11284 ];
11285 let expected_invisibles = std::iter::once(repeated_invisibles)
11286 .cycle()
11287 .take(9)
11288 .flatten()
11289 .collect::<Vec<_>>();
11290 assert_eq!(
11291 expected_invisibles.len(),
11292 input_text
11293 .chars()
11294 .filter(|initial_char| initial_char.is_whitespace())
11295 .count(),
11296 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
11297 );
11298 info!("Expected invisibles: {expected_invisibles:?}");
11299
11300 init_test(cx, |_| {});
11301
11302 // Put the same string with repeating whitespace pattern into editors of various size,
11303 // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
11304 let resize_step = 10.0;
11305 let mut editor_width = 200.0;
11306 while editor_width <= 1000.0 {
11307 for show_line_numbers in [true, false] {
11308 update_test_language_settings(cx, |s| {
11309 s.defaults.tab_size = NonZeroU32::new(tab_size);
11310 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
11311 s.defaults.preferred_line_length = Some(editor_width as u32);
11312 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
11313 });
11314
11315 let actual_invisibles = collect_invisibles_from_new_editor(
11316 cx,
11317 EditorMode::full(),
11318 &input_text,
11319 px(editor_width),
11320 show_line_numbers,
11321 );
11322
11323 // Whatever the editor size is, ensure it has the same invisible kinds in the same order
11324 // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
11325 let mut i = 0;
11326 for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
11327 i = actual_index;
11328 match expected_invisibles.get(i) {
11329 Some(expected_invisible) => match (expected_invisible, actual_invisible) {
11330 (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
11331 | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
11332 _ => {
11333 panic!(
11334 "At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}"
11335 )
11336 }
11337 },
11338 None => {
11339 panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
11340 }
11341 }
11342 }
11343 let missing_expected_invisibles = &expected_invisibles[i + 1..];
11344 assert!(
11345 missing_expected_invisibles.is_empty(),
11346 "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
11347 );
11348
11349 editor_width += resize_step;
11350 }
11351 }
11352 }
11353
11354 fn collect_invisibles_from_new_editor(
11355 cx: &mut TestAppContext,
11356 editor_mode: EditorMode,
11357 input_text: &str,
11358 editor_width: Pixels,
11359 show_line_numbers: bool,
11360 ) -> Vec<Invisible> {
11361 info!(
11362 "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
11363 f32::from(editor_width)
11364 );
11365 let window = cx.add_window(|window, cx| {
11366 let buffer = MultiBuffer::build_simple(input_text, cx);
11367 Editor::new(editor_mode, buffer, None, window, cx)
11368 });
11369 let cx = &mut VisualTestContext::from_window(*window, cx);
11370 let editor = window.root(cx).unwrap();
11371
11372 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
11373 window
11374 .update(cx, |editor, _, cx| {
11375 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
11376 editor.set_wrap_width(Some(editor_width), cx);
11377 editor.set_show_line_numbers(show_line_numbers, cx);
11378 })
11379 .unwrap();
11380 let (_, state) = cx.draw(
11381 point(px(500.), px(500.)),
11382 size(px(500.), px(500.)),
11383 |_, _| EditorElement::new(&editor, style),
11384 );
11385 state
11386 .position_map
11387 .line_layouts
11388 .iter()
11389 .flat_map(|line_with_invisibles| &line_with_invisibles.invisibles)
11390 .cloned()
11391 .collect()
11392 }
11393
11394 #[gpui::test]
11395 fn test_merge_overlapping_ranges() {
11396 let base_bg = Hsla::white();
11397 let color1 = Hsla {
11398 h: 0.0,
11399 s: 0.5,
11400 l: 0.5,
11401 a: 0.5,
11402 };
11403 let color2 = Hsla {
11404 h: 120.0,
11405 s: 0.5,
11406 l: 0.5,
11407 a: 0.5,
11408 };
11409
11410 let display_point = |col| DisplayPoint::new(DisplayRow(0), col);
11411 let cols = |v: &Vec<(Range<DisplayPoint>, Hsla)>| -> Vec<(u32, u32)> {
11412 v.iter()
11413 .map(|(r, _)| (r.start.column(), r.end.column()))
11414 .collect()
11415 };
11416
11417 // Test overlapping ranges blend colors
11418 let overlapping = vec![
11419 (display_point(5)..display_point(15), color1),
11420 (display_point(10)..display_point(20), color2),
11421 ];
11422 let result = EditorElement::merge_overlapping_ranges(overlapping, base_bg);
11423 assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
11424
11425 // Test middle segment should have blended color
11426 let blended = Hsla::blend(Hsla::blend(base_bg, color1), color2);
11427 assert_eq!(result[1].1, blended);
11428
11429 // Test adjacent same-color ranges merge
11430 let adjacent_same = vec![
11431 (display_point(5)..display_point(10), color1),
11432 (display_point(10)..display_point(15), color1),
11433 ];
11434 let result = EditorElement::merge_overlapping_ranges(adjacent_same, base_bg);
11435 assert_eq!(cols(&result), vec![(5, 15)]);
11436
11437 // Test contained range splits
11438 let contained = vec![
11439 (display_point(5)..display_point(20), color1),
11440 (display_point(10)..display_point(15), color2),
11441 ];
11442 let result = EditorElement::merge_overlapping_ranges(contained, base_bg);
11443 assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
11444
11445 // Test multiple overlaps split at every boundary
11446 let color3 = Hsla {
11447 h: 240.0,
11448 s: 0.5,
11449 l: 0.5,
11450 a: 0.5,
11451 };
11452 let complex = vec![
11453 (display_point(5)..display_point(12), color1),
11454 (display_point(8)..display_point(16), color2),
11455 (display_point(10)..display_point(14), color3),
11456 ];
11457 let result = EditorElement::merge_overlapping_ranges(complex, base_bg);
11458 assert_eq!(
11459 cols(&result),
11460 vec![(5, 8), (8, 10), (10, 12), (12, 14), (14, 16)]
11461 );
11462 }
11463
11464 #[gpui::test]
11465 fn test_bg_segments_per_row() {
11466 let base_bg = Hsla::white();
11467
11468 // Case A: selection spans three display rows: row 1 [5, end), full row 2, row 3 [0, 7)
11469 {
11470 let selection_color = Hsla {
11471 h: 200.0,
11472 s: 0.5,
11473 l: 0.5,
11474 a: 0.5,
11475 };
11476 let player_color = PlayerColor {
11477 cursor: selection_color,
11478 background: selection_color,
11479 selection: selection_color,
11480 };
11481
11482 let spanning_selection = SelectionLayout {
11483 head: DisplayPoint::new(DisplayRow(3), 7),
11484 cursor_shape: CursorShape::Bar,
11485 is_newest: true,
11486 is_local: true,
11487 range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 7),
11488 active_rows: DisplayRow(1)..DisplayRow(4),
11489 user_name: None,
11490 };
11491
11492 let selections = vec![(player_color, vec![spanning_selection])];
11493 let result = EditorElement::bg_segments_per_row(
11494 DisplayRow(0)..DisplayRow(5),
11495 &selections,
11496 &[],
11497 base_bg,
11498 );
11499
11500 assert_eq!(result.len(), 5);
11501 assert!(result[0].is_empty());
11502 assert_eq!(result[1].len(), 1);
11503 assert_eq!(result[2].len(), 1);
11504 assert_eq!(result[3].len(), 1);
11505 assert!(result[4].is_empty());
11506
11507 assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
11508 assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
11509 assert_eq!(result[1][0].0.end.column(), u32::MAX);
11510 assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
11511 assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
11512 assert_eq!(result[2][0].0.end.column(), u32::MAX);
11513 assert_eq!(result[3][0].0.start, DisplayPoint::new(DisplayRow(3), 0));
11514 assert_eq!(result[3][0].0.end, DisplayPoint::new(DisplayRow(3), 7));
11515 }
11516
11517 // Case B: selection ends exactly at the start of row 3, excluding row 3
11518 {
11519 let selection_color = Hsla {
11520 h: 120.0,
11521 s: 0.5,
11522 l: 0.5,
11523 a: 0.5,
11524 };
11525 let player_color = PlayerColor {
11526 cursor: selection_color,
11527 background: selection_color,
11528 selection: selection_color,
11529 };
11530
11531 let selection = SelectionLayout {
11532 head: DisplayPoint::new(DisplayRow(2), 0),
11533 cursor_shape: CursorShape::Bar,
11534 is_newest: true,
11535 is_local: true,
11536 range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 0),
11537 active_rows: DisplayRow(1)..DisplayRow(3),
11538 user_name: None,
11539 };
11540
11541 let selections = vec![(player_color, vec![selection])];
11542 let result = EditorElement::bg_segments_per_row(
11543 DisplayRow(0)..DisplayRow(4),
11544 &selections,
11545 &[],
11546 base_bg,
11547 );
11548
11549 assert_eq!(result.len(), 4);
11550 assert!(result[0].is_empty());
11551 assert_eq!(result[1].len(), 1);
11552 assert_eq!(result[2].len(), 1);
11553 assert!(result[3].is_empty());
11554
11555 assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
11556 assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
11557 assert_eq!(result[1][0].0.end.column(), u32::MAX);
11558 assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
11559 assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
11560 assert_eq!(result[2][0].0.end.column(), u32::MAX);
11561 }
11562 }
11563
11564 #[cfg(test)]
11565 fn generate_test_run(len: usize, color: Hsla) -> TextRun {
11566 TextRun {
11567 len,
11568 color,
11569 ..Default::default()
11570 }
11571 }
11572
11573 #[gpui::test]
11574 fn test_split_runs_by_bg_segments(cx: &mut gpui::TestAppContext) {
11575 init_test(cx, |_| {});
11576
11577 let dx = |start: u32, end: u32| {
11578 DisplayPoint::new(DisplayRow(0), start)..DisplayPoint::new(DisplayRow(0), end)
11579 };
11580
11581 let text_color = Hsla {
11582 h: 210.0,
11583 s: 0.1,
11584 l: 0.4,
11585 a: 1.0,
11586 };
11587 let bg_1 = Hsla {
11588 h: 30.0,
11589 s: 0.6,
11590 l: 0.8,
11591 a: 1.0,
11592 };
11593 let bg_2 = Hsla {
11594 h: 200.0,
11595 s: 0.6,
11596 l: 0.2,
11597 a: 1.0,
11598 };
11599 let min_contrast = 45.0;
11600 let adjusted_bg1 = ensure_minimum_contrast(text_color, bg_1, min_contrast);
11601 let adjusted_bg2 = ensure_minimum_contrast(text_color, bg_2, min_contrast);
11602
11603 // Case A: single run; disjoint segments inside the run
11604 {
11605 let runs = vec![generate_test_run(20, text_color)];
11606 let segs = vec![(dx(5, 10), bg_1), (dx(12, 16), bg_2)];
11607 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
11608 // Expected slices: [0,5) [5,10) [10,12) [12,16) [16,20)
11609 assert_eq!(
11610 out.iter().map(|r| r.len).collect::<Vec<_>>(),
11611 vec![5, 5, 2, 4, 4]
11612 );
11613 assert_eq!(out[0].color, text_color);
11614 assert_eq!(out[1].color, adjusted_bg1);
11615 assert_eq!(out[2].color, text_color);
11616 assert_eq!(out[3].color, adjusted_bg2);
11617 assert_eq!(out[4].color, text_color);
11618 }
11619
11620 // Case B: multiple runs; segment extends to end of line (u32::MAX)
11621 {
11622 let runs = vec![
11623 generate_test_run(8, text_color),
11624 generate_test_run(7, text_color),
11625 ];
11626 let segs = vec![(dx(6, u32::MAX), bg_1)];
11627 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
11628 // Expected slices across runs: [0,6) [6,8) | [0,7)
11629 assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![6, 2, 7]);
11630 assert_eq!(out[0].color, text_color);
11631 assert_eq!(out[1].color, adjusted_bg1);
11632 assert_eq!(out[2].color, adjusted_bg1);
11633 }
11634
11635 // Case C: multi-byte characters
11636 {
11637 // for text: "Hello π δΈη!"
11638 let runs = vec![
11639 generate_test_run(5, text_color), // "Hello"
11640 generate_test_run(6, text_color), // " π "
11641 generate_test_run(6, text_color), // "δΈη"
11642 generate_test_run(1, text_color), // "!"
11643 ];
11644 // selecting "π δΈ"
11645 let segs = vec![(dx(6, 14), bg_1)];
11646 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
11647 // "Hello" | " " | "π " | "δΈ" | "η" | "!"
11648 assert_eq!(
11649 out.iter().map(|r| r.len).collect::<Vec<_>>(),
11650 vec![5, 1, 5, 3, 3, 1]
11651 );
11652 assert_eq!(out[0].color, text_color); // "Hello"
11653 assert_eq!(out[2].color, adjusted_bg1); // "π "
11654 assert_eq!(out[3].color, adjusted_bg1); // "δΈ"
11655 assert_eq!(out[4].color, text_color); // "η"
11656 assert_eq!(out[5].color, text_color); // "!"
11657 }
11658
11659 // Case D: split multiple consecutive text runs with segments
11660 {
11661 let segs = vec![
11662 (dx(2, 4), bg_1), // selecting "cd"
11663 (dx(4, 8), bg_2), // selecting "efgh"
11664 (dx(9, 11), bg_1), // selecting "jk"
11665 (dx(12, 16), bg_2), // selecting "mnop"
11666 (dx(18, 19), bg_1), // selecting "s"
11667 ];
11668
11669 // for text: "abcdef"
11670 let runs = vec![
11671 generate_test_run(2, text_color), // ab
11672 generate_test_run(4, text_color), // cdef
11673 ];
11674 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
11675 // new splits "ab", "cd", "ef"
11676 assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![2, 2, 2]);
11677 assert_eq!(out[0].color, text_color);
11678 assert_eq!(out[1].color, adjusted_bg1);
11679 assert_eq!(out[2].color, adjusted_bg2);
11680
11681 // for text: "ghijklmn"
11682 let runs = vec![
11683 generate_test_run(3, text_color), // ghi
11684 generate_test_run(2, text_color), // jk
11685 generate_test_run(3, text_color), // lmn
11686 ];
11687 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 6); // 2 + 4 from first run
11688 // new splits "gh", "i", "jk", "l", "mn"
11689 assert_eq!(
11690 out.iter().map(|r| r.len).collect::<Vec<_>>(),
11691 vec![2, 1, 2, 1, 2]
11692 );
11693 assert_eq!(out[0].color, adjusted_bg2);
11694 assert_eq!(out[1].color, text_color);
11695 assert_eq!(out[2].color, adjusted_bg1);
11696 assert_eq!(out[3].color, text_color);
11697 assert_eq!(out[4].color, adjusted_bg2);
11698
11699 // for text: "opqrs"
11700 let runs = vec![
11701 generate_test_run(1, text_color), // o
11702 generate_test_run(4, text_color), // pqrs
11703 ];
11704 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 14); // 6 + 3 + 2 + 3 from first two runs
11705 // new splits "o", "p", "qr", "s"
11706 assert_eq!(
11707 out.iter().map(|r| r.len).collect::<Vec<_>>(),
11708 vec![1, 1, 2, 1]
11709 );
11710 assert_eq!(out[0].color, adjusted_bg2);
11711 assert_eq!(out[1].color, adjusted_bg2);
11712 assert_eq!(out[2].color, text_color);
11713 assert_eq!(out[3].color, adjusted_bg1);
11714 }
11715 }
11716}