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 .gap_2()
3907 .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
3908 .pl_0p5()
3909 .pr_5()
3910 .rounded_sm()
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 header.child(
3932 div()
3933 .hover(|style| style.bg(colors.element_selected))
3934 .rounded_xs()
3935 .child(
3936 ButtonLike::new("toggle-buffer-fold")
3937 .style(ui::ButtonStyle::Transparent)
3938 .height(px(28.).into())
3939 .width(px(28.))
3940 .children(toggle_chevron_icon)
3941 .tooltip({
3942 let focus_handle = focus_handle.clone();
3943 move |_window, cx| {
3944 Tooltip::with_meta_in(
3945 "Toggle Excerpt Fold",
3946 Some(&ToggleFold),
3947 format!(
3948 "{} to toggle all",
3949 text_for_keystroke(
3950 &Modifiers::alt(),
3951 "click",
3952 cx
3953 )
3954 ),
3955 &focus_handle,
3956 cx,
3957 )
3958 }
3959 })
3960 .on_click(move |event, window, cx| {
3961 if event.modifiers().alt {
3962 // Alt+click toggles all buffers
3963 editor.update(cx, |editor, cx| {
3964 editor.toggle_fold_all(
3965 &ToggleFoldAll,
3966 window,
3967 cx,
3968 );
3969 });
3970 } else {
3971 // Regular click toggles single buffer
3972 if is_folded {
3973 editor.update(cx, |editor, cx| {
3974 editor.unfold_buffer(buffer_id, cx);
3975 });
3976 } else {
3977 editor.update(cx, |editor, cx| {
3978 editor.fold_buffer(buffer_id, cx);
3979 });
3980 }
3981 }
3982 }),
3983 ),
3984 )
3985 })
3986 .children(
3987 editor
3988 .addons
3989 .values()
3990 .filter_map(|addon| {
3991 addon.render_buffer_header_controls(for_excerpt, window, cx)
3992 })
3993 .take(1),
3994 )
3995 .child(h_flex().size(px(12.0)).justify_center().children(indicator))
3996 .child(
3997 h_flex()
3998 .cursor_pointer()
3999 .id("path header block")
4000 .size_full()
4001 .justify_between()
4002 .overflow_hidden()
4003 .child(h_flex().gap_2().map(|path_header| {
4004 let filename = filename
4005 .map(SharedString::from)
4006 .unwrap_or_else(|| "untitled".into());
4007
4008 path_header
4009 .when(ItemSettings::get_global(cx).file_icons, |el| {
4010 let path = path::Path::new(filename.as_str());
4011 let icon =
4012 FileIcons::get_icon(path, cx).unwrap_or_default();
4013 let icon = Icon::from_path(icon).color(Color::Muted);
4014 el.child(icon)
4015 })
4016 .child(
4017 ButtonLike::new("filename-button")
4018 .style(ButtonStyle::Subtle)
4019 .child(
4020 div()
4021 .child(
4022 Label::new(filename)
4023 .single_line()
4024 .color(file_status_label_color(
4025 file_status,
4026 ))
4027 .when(
4028 file_status.is_some_and(|s| {
4029 s.is_deleted()
4030 }),
4031 |label| label.strikethrough(),
4032 ),
4033 )
4034 .group_hover("", |div| div.underline()),
4035 )
4036 .on_click(window.listener_for(&self.editor, {
4037 let jump_data = jump_data.clone();
4038 move |editor, e: &ClickEvent, window, cx| {
4039 editor.open_excerpts_common(
4040 Some(jump_data.clone()),
4041 e.modifiers().secondary(),
4042 window,
4043 cx,
4044 );
4045 }
4046 })),
4047 )
4048 .when_some(parent_path, |then, path| {
4049 then.child(div().child(path).text_color(
4050 if file_status.is_some_and(FileStatus::is_deleted) {
4051 colors.text_disabled
4052 } else {
4053 colors.text_muted
4054 },
4055 ))
4056 })
4057 }))
4058 .when(
4059 can_open_excerpts && is_selected && relative_path.is_some(),
4060 |el| {
4061 el.child(
4062 ButtonLike::new("open-file-button")
4063 .style(ButtonStyle::OutlinedGhost)
4064 .child(
4065 h_flex()
4066 .gap_2p5()
4067 .child(Label::new("Open file"))
4068 .child(KeyBinding::for_action_in(
4069 &OpenExcerpts,
4070 &focus_handle,
4071 cx,
4072 )),
4073 )
4074 .on_click(window.listener_for(&self.editor, {
4075 let jump_data = jump_data.clone();
4076 move |editor, e: &ClickEvent, window, cx| {
4077 editor.open_excerpts_common(
4078 Some(jump_data.clone()),
4079 e.modifiers().secondary(),
4080 window,
4081 cx,
4082 );
4083 }
4084 })),
4085 )
4086 },
4087 )
4088 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
4089 .on_click(window.listener_for(&self.editor, {
4090 let buffer_id = for_excerpt.buffer_id;
4091 move |editor, _e: &ClickEvent, _window, cx| {
4092 if is_folded {
4093 editor.unfold_buffer(buffer_id, cx);
4094 } else {
4095 editor.fold_buffer(buffer_id, cx);
4096 }
4097 }
4098 })),
4099 ),
4100 );
4101
4102 let file = for_excerpt.buffer.file().cloned();
4103 let editor = self.editor.clone();
4104 right_click_menu("buffer-header-context-menu")
4105 .trigger(move |_, _, _| header)
4106 .menu(move |window, cx| {
4107 let menu_context = focus_handle.clone();
4108 let editor = editor.clone();
4109 let file = file.clone();
4110 ContextMenu::build(window, cx, move |mut menu, window, cx| {
4111 if let Some(file) = file
4112 && let Some(project) = editor.read(cx).project()
4113 && let Some(worktree) =
4114 project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
4115 {
4116 let path_style = file.path_style(cx);
4117 let worktree = worktree.read(cx);
4118 let relative_path = file.path();
4119 let entry_for_path = worktree.entry_for_path(relative_path);
4120 let abs_path = entry_for_path.map(|e| {
4121 e.canonical_path.as_deref().map_or_else(
4122 || worktree.absolutize(relative_path),
4123 Path::to_path_buf,
4124 )
4125 });
4126 let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
4127
4128 let parent_abs_path = abs_path
4129 .as_ref()
4130 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
4131 let relative_path = has_relative_path
4132 .then_some(relative_path)
4133 .map(ToOwned::to_owned);
4134
4135 let visible_in_project_panel =
4136 relative_path.is_some() && worktree.is_visible();
4137 let reveal_in_project_panel = entry_for_path
4138 .filter(|_| visible_in_project_panel)
4139 .map(|entry| entry.id);
4140 menu = menu
4141 .when_some(abs_path, |menu, abs_path| {
4142 menu.entry(
4143 "Copy Path",
4144 Some(Box::new(zed_actions::workspace::CopyPath)),
4145 window.handler_for(&editor, move |_, _, cx| {
4146 cx.write_to_clipboard(ClipboardItem::new_string(
4147 abs_path.to_string_lossy().into_owned(),
4148 ));
4149 }),
4150 )
4151 })
4152 .when_some(relative_path, |menu, relative_path| {
4153 menu.entry(
4154 "Copy Relative Path",
4155 Some(Box::new(zed_actions::workspace::CopyRelativePath)),
4156 window.handler_for(&editor, move |_, _, cx| {
4157 cx.write_to_clipboard(ClipboardItem::new_string(
4158 relative_path.display(path_style).to_string(),
4159 ));
4160 }),
4161 )
4162 })
4163 .when(
4164 reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
4165 |menu| menu.separator(),
4166 )
4167 .when_some(reveal_in_project_panel, |menu, entry_id| {
4168 menu.entry(
4169 "Reveal In Project Panel",
4170 Some(Box::new(RevealInProjectPanel::default())),
4171 window.handler_for(&editor, move |editor, _, cx| {
4172 if let Some(project) = &mut editor.project {
4173 project.update(cx, |_, cx| {
4174 cx.emit(project::Event::RevealInProjectPanel(
4175 entry_id,
4176 ))
4177 });
4178 }
4179 }),
4180 )
4181 })
4182 .when_some(parent_abs_path, |menu, parent_abs_path| {
4183 menu.entry(
4184 "Open in Terminal",
4185 Some(Box::new(OpenInTerminal)),
4186 window.handler_for(&editor, move |_, window, cx| {
4187 window.dispatch_action(
4188 OpenTerminal {
4189 working_directory: parent_abs_path.clone(),
4190 }
4191 .boxed_clone(),
4192 cx,
4193 );
4194 }),
4195 )
4196 });
4197 }
4198
4199 menu.context(menu_context)
4200 })
4201 })
4202 }
4203
4204 fn render_blocks(
4205 &self,
4206 rows: Range<DisplayRow>,
4207 snapshot: &EditorSnapshot,
4208 hitbox: &Hitbox,
4209 text_hitbox: &Hitbox,
4210 editor_width: Pixels,
4211 scroll_width: &mut Pixels,
4212 editor_margins: &EditorMargins,
4213 em_width: Pixels,
4214 text_x: Pixels,
4215 line_height: Pixels,
4216 line_layouts: &mut [LineWithInvisibles],
4217 selections: &[Selection<Point>],
4218 selected_buffer_ids: &Vec<BufferId>,
4219 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
4220 sticky_header_excerpt_id: Option<ExcerptId>,
4221 window: &mut Window,
4222 cx: &mut App,
4223 ) -> Result<(Vec<BlockLayout>, HashMap<DisplayRow, bool>), HashMap<CustomBlockId, u32>> {
4224 let (fixed_blocks, non_fixed_blocks) = snapshot
4225 .blocks_in_range(rows.clone())
4226 .partition::<Vec<_>, _>(|(_, block)| block.style() == BlockStyle::Fixed);
4227
4228 let mut focused_block = self
4229 .editor
4230 .update(cx, |editor, _| editor.take_focused_block());
4231 let mut fixed_block_max_width = Pixels::ZERO;
4232 let mut blocks = Vec::new();
4233 let mut resized_blocks = HashMap::default();
4234 let mut row_block_types = HashMap::default();
4235
4236 for (row, block) in fixed_blocks {
4237 let block_id = block.id();
4238
4239 if focused_block.as_ref().is_some_and(|b| b.id == block_id) {
4240 focused_block = None;
4241 }
4242
4243 if let Some((element, element_size, row, x_offset)) = self.render_block(
4244 block,
4245 AvailableSpace::MinContent,
4246 block_id,
4247 row,
4248 snapshot,
4249 text_x,
4250 &rows,
4251 line_layouts,
4252 editor_margins,
4253 line_height,
4254 em_width,
4255 text_hitbox,
4256 editor_width,
4257 scroll_width,
4258 &mut resized_blocks,
4259 &mut row_block_types,
4260 selections,
4261 selected_buffer_ids,
4262 is_row_soft_wrapped,
4263 sticky_header_excerpt_id,
4264 window,
4265 cx,
4266 ) {
4267 fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
4268 blocks.push(BlockLayout {
4269 id: block_id,
4270 x_offset,
4271 row: Some(row),
4272 element,
4273 available_space: size(AvailableSpace::MinContent, element_size.height.into()),
4274 style: BlockStyle::Fixed,
4275 overlaps_gutter: true,
4276 is_buffer_header: block.is_buffer_header(),
4277 });
4278 }
4279 }
4280
4281 for (row, block) in non_fixed_blocks {
4282 let style = block.style();
4283 let width = match (style, block.place_near()) {
4284 (_, true) => AvailableSpace::MinContent,
4285 (BlockStyle::Sticky, _) => hitbox.size.width.into(),
4286 (BlockStyle::Flex, _) => hitbox
4287 .size
4288 .width
4289 .max(fixed_block_max_width)
4290 .max(editor_margins.gutter.width + *scroll_width)
4291 .into(),
4292 (BlockStyle::Fixed, _) => unreachable!(),
4293 };
4294 let block_id = block.id();
4295
4296 if focused_block.as_ref().is_some_and(|b| b.id == block_id) {
4297 focused_block = None;
4298 }
4299
4300 if let Some((element, element_size, row, x_offset)) = self.render_block(
4301 block,
4302 width,
4303 block_id,
4304 row,
4305 snapshot,
4306 text_x,
4307 &rows,
4308 line_layouts,
4309 editor_margins,
4310 line_height,
4311 em_width,
4312 text_hitbox,
4313 editor_width,
4314 scroll_width,
4315 &mut resized_blocks,
4316 &mut row_block_types,
4317 selections,
4318 selected_buffer_ids,
4319 is_row_soft_wrapped,
4320 sticky_header_excerpt_id,
4321 window,
4322 cx,
4323 ) {
4324 blocks.push(BlockLayout {
4325 id: block_id,
4326 x_offset,
4327 row: Some(row),
4328 element,
4329 available_space: size(width, element_size.height.into()),
4330 style,
4331 overlaps_gutter: !block.place_near(),
4332 is_buffer_header: block.is_buffer_header(),
4333 });
4334 }
4335 }
4336
4337 if let Some(focused_block) = focused_block
4338 && let Some(focus_handle) = focused_block.focus_handle.upgrade()
4339 && focus_handle.is_focused(window)
4340 && let Some(block) = snapshot.block_for_id(focused_block.id)
4341 {
4342 let style = block.style();
4343 let width = match style {
4344 BlockStyle::Fixed => AvailableSpace::MinContent,
4345 BlockStyle::Flex => AvailableSpace::Definite(
4346 hitbox
4347 .size
4348 .width
4349 .max(fixed_block_max_width)
4350 .max(editor_margins.gutter.width + *scroll_width),
4351 ),
4352 BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
4353 };
4354
4355 if let Some((element, element_size, _, x_offset)) = self.render_block(
4356 &block,
4357 width,
4358 focused_block.id,
4359 rows.end,
4360 snapshot,
4361 text_x,
4362 &rows,
4363 line_layouts,
4364 editor_margins,
4365 line_height,
4366 em_width,
4367 text_hitbox,
4368 editor_width,
4369 scroll_width,
4370 &mut resized_blocks,
4371 &mut row_block_types,
4372 selections,
4373 selected_buffer_ids,
4374 is_row_soft_wrapped,
4375 sticky_header_excerpt_id,
4376 window,
4377 cx,
4378 ) {
4379 blocks.push(BlockLayout {
4380 id: block.id(),
4381 x_offset,
4382 row: None,
4383 element,
4384 available_space: size(width, element_size.height.into()),
4385 style,
4386 overlaps_gutter: true,
4387 is_buffer_header: block.is_buffer_header(),
4388 });
4389 }
4390 }
4391
4392 if resized_blocks.is_empty() {
4393 *scroll_width =
4394 (*scroll_width).max(fixed_block_max_width - editor_margins.gutter.width);
4395 Ok((blocks, row_block_types))
4396 } else {
4397 Err(resized_blocks)
4398 }
4399 }
4400
4401 fn layout_blocks(
4402 &self,
4403 blocks: &mut Vec<BlockLayout>,
4404 hitbox: &Hitbox,
4405 line_height: Pixels,
4406 scroll_position: gpui::Point<ScrollOffset>,
4407 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
4408 window: &mut Window,
4409 cx: &mut App,
4410 ) {
4411 for block in blocks {
4412 let mut origin = if let Some(row) = block.row {
4413 hitbox.origin
4414 + point(
4415 block.x_offset,
4416 Pixels::from(
4417 (row.as_f64() - scroll_position.y)
4418 * ScrollPixelOffset::from(line_height),
4419 ),
4420 )
4421 } else {
4422 // Position the block outside the visible area
4423 hitbox.origin + point(Pixels::ZERO, hitbox.size.height)
4424 };
4425
4426 if !matches!(block.style, BlockStyle::Sticky) {
4427 origin += point(Pixels::from(-scroll_pixel_position.x), Pixels::ZERO);
4428 }
4429
4430 let focus_handle =
4431 block
4432 .element
4433 .prepaint_as_root(origin, block.available_space, window, cx);
4434
4435 if let Some(focus_handle) = focus_handle {
4436 self.editor.update(cx, |editor, _cx| {
4437 editor.set_focused_block(FocusedBlock {
4438 id: block.id,
4439 focus_handle: focus_handle.downgrade(),
4440 });
4441 });
4442 }
4443 }
4444 }
4445
4446 fn layout_sticky_buffer_header(
4447 &self,
4448 StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>,
4449 scroll_position: gpui::Point<ScrollOffset>,
4450 line_height: Pixels,
4451 right_margin: Pixels,
4452 snapshot: &EditorSnapshot,
4453 hitbox: &Hitbox,
4454 selected_buffer_ids: &Vec<BufferId>,
4455 blocks: &[BlockLayout],
4456 window: &mut Window,
4457 cx: &mut App,
4458 ) -> AnyElement {
4459 let jump_data = header_jump_data(
4460 snapshot,
4461 DisplayRow(scroll_position.y as u32),
4462 FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
4463 excerpt,
4464 );
4465
4466 let editor_bg_color = cx.theme().colors().editor_background;
4467
4468 let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
4469
4470 let available_width = hitbox.bounds.size.width - right_margin;
4471
4472 let mut header = v_flex()
4473 .w_full()
4474 .relative()
4475 .child(
4476 div()
4477 .w(available_width)
4478 .h(FILE_HEADER_HEIGHT as f32 * line_height)
4479 .bg(linear_gradient(
4480 0.,
4481 linear_color_stop(editor_bg_color.opacity(0.), 0.),
4482 linear_color_stop(editor_bg_color, 0.6),
4483 ))
4484 .absolute()
4485 .top_0(),
4486 )
4487 .child(
4488 self.render_buffer_header(excerpt, false, selected, true, jump_data, window, cx)
4489 .into_any_element(),
4490 )
4491 .into_any_element();
4492
4493 let mut origin = hitbox.origin;
4494 // Move floating header up to avoid colliding with the next buffer header.
4495 for block in blocks.iter() {
4496 if !block.is_buffer_header {
4497 continue;
4498 }
4499
4500 let Some(display_row) = block.row.filter(|row| row.0 > scroll_position.y as u32) else {
4501 continue;
4502 };
4503
4504 let max_row = display_row.0.saturating_sub(FILE_HEADER_HEIGHT);
4505 let offset = scroll_position.y - max_row as f64;
4506
4507 if offset > 0.0 {
4508 origin.y -= Pixels::from(offset * ScrollPixelOffset::from(line_height));
4509 }
4510 break;
4511 }
4512
4513 let size = size(
4514 AvailableSpace::Definite(available_width),
4515 AvailableSpace::MinContent,
4516 );
4517
4518 header.prepaint_as_root(origin, size, window, cx);
4519
4520 header
4521 }
4522
4523 fn layout_cursor_popovers(
4524 &self,
4525 line_height: Pixels,
4526 text_hitbox: &Hitbox,
4527 content_origin: gpui::Point<Pixels>,
4528 right_margin: Pixels,
4529 start_row: DisplayRow,
4530 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
4531 line_layouts: &[LineWithInvisibles],
4532 cursor: DisplayPoint,
4533 cursor_point: Point,
4534 style: &EditorStyle,
4535 window: &mut Window,
4536 cx: &mut App,
4537 ) -> Option<ContextMenuLayout> {
4538 let mut min_menu_height = Pixels::ZERO;
4539 let mut max_menu_height = Pixels::ZERO;
4540 let mut height_above_menu = Pixels::ZERO;
4541 let height_below_menu = Pixels::ZERO;
4542 let mut edit_prediction_popover_visible = false;
4543 let mut context_menu_visible = false;
4544 let context_menu_placement;
4545
4546 {
4547 let editor = self.editor.read(cx);
4548 if editor.edit_prediction_visible_in_cursor_popover(editor.has_active_edit_prediction())
4549 {
4550 height_above_menu +=
4551 editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING;
4552 edit_prediction_popover_visible = true;
4553 }
4554
4555 if editor.context_menu_visible()
4556 && let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin()
4557 {
4558 let (min_height_in_lines, max_height_in_lines) = editor
4559 .context_menu_options
4560 .as_ref()
4561 .map_or((3, 12), |options| {
4562 (options.min_entries_visible, options.max_entries_visible)
4563 });
4564
4565 min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING;
4566 max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING;
4567 context_menu_visible = true;
4568 }
4569 context_menu_placement = editor
4570 .context_menu_options
4571 .as_ref()
4572 .and_then(|options| options.placement.clone());
4573 }
4574
4575 let visible = edit_prediction_popover_visible || context_menu_visible;
4576 if !visible {
4577 return None;
4578 }
4579
4580 let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
4581 let target_position = content_origin
4582 + gpui::Point {
4583 x: cmp::max(
4584 px(0.),
4585 Pixels::from(
4586 ScrollPixelOffset::from(
4587 cursor_row_layout.x_for_index(cursor.column() as usize),
4588 ) - scroll_pixel_position.x,
4589 ),
4590 ),
4591 y: cmp::max(
4592 px(0.),
4593 Pixels::from(
4594 cursor.row().next_row().as_f64() * ScrollPixelOffset::from(line_height)
4595 - scroll_pixel_position.y,
4596 ),
4597 ),
4598 };
4599
4600 let viewport_bounds =
4601 Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
4602 right: -right_margin - MENU_GAP,
4603 ..Default::default()
4604 });
4605
4606 let min_height = height_above_menu + min_menu_height + height_below_menu;
4607 let max_height = height_above_menu + max_menu_height + height_below_menu;
4608 let (laid_out_popovers, y_flipped) = self.layout_popovers_above_or_below_line(
4609 target_position,
4610 line_height,
4611 min_height,
4612 max_height,
4613 context_menu_placement,
4614 text_hitbox,
4615 viewport_bounds,
4616 window,
4617 cx,
4618 |height, max_width_for_stable_x, y_flipped, window, cx| {
4619 // First layout the menu to get its size - others can be at least this wide.
4620 let context_menu = if context_menu_visible {
4621 let menu_height = if y_flipped {
4622 height - height_below_menu
4623 } else {
4624 height - height_above_menu
4625 };
4626 let mut element = self
4627 .render_context_menu(line_height, menu_height, window, cx)
4628 .expect("Visible context menu should always render.");
4629 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
4630 Some((CursorPopoverType::CodeContextMenu, element, size))
4631 } else {
4632 None
4633 };
4634 let min_width = context_menu
4635 .as_ref()
4636 .map_or(px(0.), |(_, _, size)| size.width);
4637 let max_width = max_width_for_stable_x.max(
4638 context_menu
4639 .as_ref()
4640 .map_or(px(0.), |(_, _, size)| size.width),
4641 );
4642
4643 let edit_prediction = if edit_prediction_popover_visible {
4644 self.editor.update(cx, move |editor, cx| {
4645 let accept_binding =
4646 editor.accept_edit_prediction_keybind(false, window, cx);
4647 let mut element = editor.render_edit_prediction_cursor_popover(
4648 min_width,
4649 max_width,
4650 cursor_point,
4651 style,
4652 accept_binding.keystroke(),
4653 window,
4654 cx,
4655 )?;
4656 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
4657 Some((CursorPopoverType::EditPrediction, element, size))
4658 })
4659 } else {
4660 None
4661 };
4662 vec![edit_prediction, context_menu]
4663 .into_iter()
4664 .flatten()
4665 .collect::<Vec<_>>()
4666 },
4667 )?;
4668
4669 let (menu_ix, (_, menu_bounds)) = laid_out_popovers
4670 .iter()
4671 .find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))?;
4672 let last_ix = laid_out_popovers.len() - 1;
4673 let menu_is_last = menu_ix == last_ix;
4674 let first_popover_bounds = laid_out_popovers[0].1;
4675 let last_popover_bounds = laid_out_popovers[last_ix].1;
4676
4677 // Bounds to layout the aside around. When y_flipped, the aside goes either above or to the
4678 // right, and otherwise it goes below or to the right.
4679 let mut target_bounds = Bounds::from_corners(
4680 first_popover_bounds.origin,
4681 last_popover_bounds.bottom_right(),
4682 );
4683 target_bounds.size.width = menu_bounds.size.width;
4684
4685 // Like `target_bounds`, but with the max height it could occupy. Choosing an aside position
4686 // based on this is preferred for layout stability.
4687 let mut max_target_bounds = target_bounds;
4688 max_target_bounds.size.height = max_height;
4689 if y_flipped {
4690 max_target_bounds.origin.y -= max_height - target_bounds.size.height;
4691 }
4692
4693 // Add spacing around `target_bounds` and `max_target_bounds`.
4694 let mut extend_amount = Edges::all(MENU_GAP);
4695 if y_flipped {
4696 extend_amount.bottom = line_height;
4697 } else {
4698 extend_amount.top = line_height;
4699 }
4700 let target_bounds = target_bounds.extend(extend_amount);
4701 let max_target_bounds = max_target_bounds.extend(extend_amount);
4702
4703 let must_place_above_or_below =
4704 if y_flipped && !menu_is_last && menu_bounds.size.height < max_menu_height {
4705 laid_out_popovers[menu_ix + 1..]
4706 .iter()
4707 .any(|(_, popover_bounds)| popover_bounds.size.width > menu_bounds.size.width)
4708 } else {
4709 false
4710 };
4711
4712 let aside_bounds = self.layout_context_menu_aside(
4713 y_flipped,
4714 *menu_bounds,
4715 target_bounds,
4716 max_target_bounds,
4717 max_menu_height,
4718 must_place_above_or_below,
4719 text_hitbox,
4720 viewport_bounds,
4721 window,
4722 cx,
4723 );
4724
4725 if let Some(menu_bounds) = laid_out_popovers.iter().find_map(|(popover_type, bounds)| {
4726 if matches!(popover_type, CursorPopoverType::CodeContextMenu) {
4727 Some(*bounds)
4728 } else {
4729 None
4730 }
4731 }) {
4732 let bounds = if let Some(aside_bounds) = aside_bounds {
4733 menu_bounds.union(&aside_bounds)
4734 } else {
4735 menu_bounds
4736 };
4737 return Some(ContextMenuLayout { y_flipped, bounds });
4738 }
4739
4740 None
4741 }
4742
4743 fn layout_gutter_menu(
4744 &self,
4745 line_height: Pixels,
4746 text_hitbox: &Hitbox,
4747 content_origin: gpui::Point<Pixels>,
4748 right_margin: Pixels,
4749 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
4750 gutter_overshoot: Pixels,
4751 window: &mut Window,
4752 cx: &mut App,
4753 ) {
4754 let editor = self.editor.read(cx);
4755 if !editor.context_menu_visible() {
4756 return;
4757 }
4758 let Some(crate::ContextMenuOrigin::GutterIndicator(gutter_row)) =
4759 editor.context_menu_origin()
4760 else {
4761 return;
4762 };
4763 // Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the
4764 // indicator than just a plain first column of the text field.
4765 let target_position = content_origin
4766 + gpui::Point {
4767 x: -gutter_overshoot,
4768 y: Pixels::from(
4769 gutter_row.next_row().as_f64() * ScrollPixelOffset::from(line_height)
4770 - scroll_pixel_position.y,
4771 ),
4772 };
4773
4774 let (min_height_in_lines, max_height_in_lines) = editor
4775 .context_menu_options
4776 .as_ref()
4777 .map_or((3, 12), |options| {
4778 (options.min_entries_visible, options.max_entries_visible)
4779 });
4780
4781 let min_height = line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING;
4782 let max_height = line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING;
4783 let viewport_bounds =
4784 Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
4785 right: -right_margin - MENU_GAP,
4786 ..Default::default()
4787 });
4788 self.layout_popovers_above_or_below_line(
4789 target_position,
4790 line_height,
4791 min_height,
4792 max_height,
4793 editor
4794 .context_menu_options
4795 .as_ref()
4796 .and_then(|options| options.placement.clone()),
4797 text_hitbox,
4798 viewport_bounds,
4799 window,
4800 cx,
4801 move |height, _max_width_for_stable_x, _, window, cx| {
4802 let mut element = self
4803 .render_context_menu(line_height, height, window, cx)
4804 .expect("Visible context menu should always render.");
4805 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
4806 vec![(CursorPopoverType::CodeContextMenu, element, size)]
4807 },
4808 );
4809 }
4810
4811 fn layout_popovers_above_or_below_line(
4812 &self,
4813 target_position: gpui::Point<Pixels>,
4814 line_height: Pixels,
4815 min_height: Pixels,
4816 max_height: Pixels,
4817 placement: Option<ContextMenuPlacement>,
4818 text_hitbox: &Hitbox,
4819 viewport_bounds: Bounds<Pixels>,
4820 window: &mut Window,
4821 cx: &mut App,
4822 make_sized_popovers: impl FnOnce(
4823 Pixels,
4824 Pixels,
4825 bool,
4826 &mut Window,
4827 &mut App,
4828 ) -> Vec<(CursorPopoverType, AnyElement, Size<Pixels>)>,
4829 ) -> Option<(Vec<(CursorPopoverType, Bounds<Pixels>)>, bool)> {
4830 let text_style = TextStyleRefinement {
4831 line_height: Some(DefiniteLength::Fraction(
4832 BufferLineHeight::Comfortable.value(),
4833 )),
4834 ..Default::default()
4835 };
4836 window.with_text_style(Some(text_style), |window| {
4837 // If the max height won't fit below and there is more space above, put it above the line.
4838 let bottom_y_when_flipped = target_position.y - line_height;
4839 let available_above = bottom_y_when_flipped - text_hitbox.top();
4840 let available_below = text_hitbox.bottom() - target_position.y;
4841 let y_overflows_below = max_height > available_below;
4842 let mut y_flipped = match placement {
4843 Some(ContextMenuPlacement::Above) => true,
4844 Some(ContextMenuPlacement::Below) => false,
4845 None => y_overflows_below && available_above > available_below,
4846 };
4847 let mut height = cmp::min(
4848 max_height,
4849 if y_flipped {
4850 available_above
4851 } else {
4852 available_below
4853 },
4854 );
4855
4856 // If the min height doesn't fit within text bounds, instead fit within the window.
4857 if height < min_height {
4858 let available_above = bottom_y_when_flipped;
4859 let available_below = viewport_bounds.bottom() - target_position.y;
4860 let (y_flipped_override, height_override) = match placement {
4861 Some(ContextMenuPlacement::Above) => {
4862 (true, cmp::min(available_above, min_height))
4863 }
4864 Some(ContextMenuPlacement::Below) => {
4865 (false, cmp::min(available_below, min_height))
4866 }
4867 None => {
4868 if available_below > min_height {
4869 (false, min_height)
4870 } else if available_above > min_height {
4871 (true, min_height)
4872 } else if available_above > available_below {
4873 (true, available_above)
4874 } else {
4875 (false, available_below)
4876 }
4877 }
4878 };
4879 y_flipped = y_flipped_override;
4880 height = height_override;
4881 }
4882
4883 let max_width_for_stable_x = viewport_bounds.right() - target_position.x;
4884
4885 // TODO: Use viewport_bounds.width as a max width so that it doesn't get clipped on the left
4886 // for very narrow windows.
4887 let popovers =
4888 make_sized_popovers(height, max_width_for_stable_x, y_flipped, window, cx);
4889 if popovers.is_empty() {
4890 return None;
4891 }
4892
4893 let max_width = popovers
4894 .iter()
4895 .map(|(_, _, size)| size.width)
4896 .max()
4897 .unwrap_or_default();
4898
4899 let mut current_position = gpui::Point {
4900 // Snap the right edge of the list to the right edge of the window if its horizontal bounds
4901 // overflow. Include space for the scrollbar.
4902 x: target_position
4903 .x
4904 .min((viewport_bounds.right() - max_width).max(Pixels::ZERO)),
4905 y: if y_flipped {
4906 bottom_y_when_flipped
4907 } else {
4908 target_position.y
4909 },
4910 };
4911
4912 let mut laid_out_popovers = popovers
4913 .into_iter()
4914 .map(|(popover_type, element, size)| {
4915 if y_flipped {
4916 current_position.y -= size.height;
4917 }
4918 let position = current_position;
4919 window.defer_draw(element, current_position, 1);
4920 if !y_flipped {
4921 current_position.y += size.height + MENU_GAP;
4922 } else {
4923 current_position.y -= MENU_GAP;
4924 }
4925 (popover_type, Bounds::new(position, size))
4926 })
4927 .collect::<Vec<_>>();
4928
4929 if y_flipped {
4930 laid_out_popovers.reverse();
4931 }
4932
4933 Some((laid_out_popovers, y_flipped))
4934 })
4935 }
4936
4937 fn layout_context_menu_aside(
4938 &self,
4939 y_flipped: bool,
4940 menu_bounds: Bounds<Pixels>,
4941 target_bounds: Bounds<Pixels>,
4942 max_target_bounds: Bounds<Pixels>,
4943 max_height: Pixels,
4944 must_place_above_or_below: bool,
4945 text_hitbox: &Hitbox,
4946 viewport_bounds: Bounds<Pixels>,
4947 window: &mut Window,
4948 cx: &mut App,
4949 ) -> Option<Bounds<Pixels>> {
4950 let available_within_viewport = target_bounds.space_within(&viewport_bounds);
4951 let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH
4952 && !must_place_above_or_below
4953 {
4954 let max_width = cmp::min(
4955 available_within_viewport.right - px(1.),
4956 MENU_ASIDE_MAX_WIDTH,
4957 );
4958 let mut aside = self.render_context_menu_aside(
4959 size(max_width, max_height - POPOVER_Y_PADDING),
4960 window,
4961 cx,
4962 )?;
4963 let size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
4964 let right_position = point(target_bounds.right(), menu_bounds.origin.y);
4965 Some((aside, right_position, size))
4966 } else {
4967 let max_size = size(
4968 // TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
4969 // won't be needed here.
4970 cmp::min(
4971 cmp::max(menu_bounds.size.width - px(2.), MENU_ASIDE_MIN_WIDTH),
4972 viewport_bounds.right(),
4973 ),
4974 cmp::min(
4975 max_height,
4976 cmp::max(
4977 available_within_viewport.top,
4978 available_within_viewport.bottom,
4979 ),
4980 ) - POPOVER_Y_PADDING,
4981 );
4982 let mut aside = self.render_context_menu_aside(max_size, window, cx)?;
4983 let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
4984
4985 let top_position = point(
4986 menu_bounds.origin.x,
4987 target_bounds.top() - actual_size.height,
4988 );
4989 let bottom_position = point(menu_bounds.origin.x, target_bounds.bottom());
4990
4991 let fit_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
4992 // Prefer to fit on the same side of the line as the menu, then on the other side of
4993 // the line.
4994 if !y_flipped && wanted.height < available.bottom {
4995 Some(bottom_position)
4996 } else if !y_flipped && wanted.height < available.top {
4997 Some(top_position)
4998 } else if y_flipped && wanted.height < available.top {
4999 Some(top_position)
5000 } else if y_flipped && wanted.height < available.bottom {
5001 Some(bottom_position)
5002 } else {
5003 None
5004 }
5005 };
5006
5007 // Prefer choosing a direction using max sizes rather than actual size for stability.
5008 let available_within_text = max_target_bounds.space_within(&text_hitbox.bounds);
5009 let wanted = size(MENU_ASIDE_MAX_WIDTH, max_height);
5010 let aside_position = fit_within(available_within_text, wanted)
5011 // Fallback: fit max size in window.
5012 .or_else(|| fit_within(max_target_bounds.space_within(&viewport_bounds), wanted))
5013 // Fallback: fit actual size in window.
5014 .or_else(|| fit_within(available_within_viewport, actual_size));
5015
5016 aside_position.map(|position| (aside, position, actual_size))
5017 };
5018
5019 // Skip drawing if it doesn't fit anywhere.
5020 if let Some((aside, position, size)) = positioned_aside {
5021 let aside_bounds = Bounds::new(position, size);
5022 window.defer_draw(aside, position, 2);
5023 return Some(aside_bounds);
5024 }
5025
5026 None
5027 }
5028
5029 fn render_context_menu(
5030 &self,
5031 line_height: Pixels,
5032 height: Pixels,
5033 window: &mut Window,
5034 cx: &mut App,
5035 ) -> Option<AnyElement> {
5036 let max_height_in_lines = ((height - POPOVER_Y_PADDING) / line_height).floor() as u32;
5037 self.editor.update(cx, |editor, cx| {
5038 editor.render_context_menu(&self.style, max_height_in_lines, window, cx)
5039 })
5040 }
5041
5042 fn render_context_menu_aside(
5043 &self,
5044 max_size: Size<Pixels>,
5045 window: &mut Window,
5046 cx: &mut App,
5047 ) -> Option<AnyElement> {
5048 if max_size.width < px(100.) || max_size.height < px(12.) {
5049 None
5050 } else {
5051 self.editor.update(cx, |editor, cx| {
5052 editor.render_context_menu_aside(max_size, window, cx)
5053 })
5054 }
5055 }
5056
5057 fn layout_mouse_context_menu(
5058 &self,
5059 editor_snapshot: &EditorSnapshot,
5060 visible_range: Range<DisplayRow>,
5061 content_origin: gpui::Point<Pixels>,
5062 window: &mut Window,
5063 cx: &mut App,
5064 ) -> Option<AnyElement> {
5065 let position = self.editor.update(cx, |editor, _cx| {
5066 let visible_start_point = editor.display_to_pixel_point(
5067 DisplayPoint::new(visible_range.start, 0),
5068 editor_snapshot,
5069 window,
5070 )?;
5071 let visible_end_point = editor.display_to_pixel_point(
5072 DisplayPoint::new(visible_range.end, 0),
5073 editor_snapshot,
5074 window,
5075 )?;
5076
5077 let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
5078 let (source_display_point, position) = match mouse_context_menu.position {
5079 MenuPosition::PinnedToScreen(point) => (None, point),
5080 MenuPosition::PinnedToEditor { source, offset } => {
5081 let source_display_point = source.to_display_point(editor_snapshot);
5082 let source_point = editor.to_pixel_point(source, editor_snapshot, window)?;
5083 let position = content_origin + source_point + offset;
5084 (Some(source_display_point), position)
5085 }
5086 };
5087
5088 let source_included = source_display_point.is_none_or(|source_display_point| {
5089 visible_range
5090 .to_inclusive()
5091 .contains(&source_display_point.row())
5092 });
5093 let position_included =
5094 visible_start_point.y <= position.y && position.y <= visible_end_point.y;
5095 if !source_included && !position_included {
5096 None
5097 } else {
5098 Some(position)
5099 }
5100 })?;
5101
5102 let text_style = TextStyleRefinement {
5103 line_height: Some(DefiniteLength::Fraction(
5104 BufferLineHeight::Comfortable.value(),
5105 )),
5106 ..Default::default()
5107 };
5108 window.with_text_style(Some(text_style), |window| {
5109 let mut element = self.editor.read_with(cx, |editor, _| {
5110 let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
5111 let context_menu = mouse_context_menu.context_menu.clone();
5112
5113 Some(
5114 deferred(
5115 anchored()
5116 .position(position)
5117 .child(context_menu)
5118 .anchor(Corner::TopLeft)
5119 .snap_to_window_with_margin(px(8.)),
5120 )
5121 .with_priority(1)
5122 .into_any(),
5123 )
5124 })?;
5125
5126 element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx);
5127 Some(element)
5128 })
5129 }
5130
5131 fn layout_hover_popovers(
5132 &self,
5133 snapshot: &EditorSnapshot,
5134 hitbox: &Hitbox,
5135 visible_display_row_range: Range<DisplayRow>,
5136 content_origin: gpui::Point<Pixels>,
5137 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
5138 line_layouts: &[LineWithInvisibles],
5139 line_height: Pixels,
5140 em_width: Pixels,
5141 context_menu_layout: Option<ContextMenuLayout>,
5142 window: &mut Window,
5143 cx: &mut App,
5144 ) {
5145 struct MeasuredHoverPopover {
5146 element: AnyElement,
5147 size: Size<Pixels>,
5148 horizontal_offset: Pixels,
5149 }
5150
5151 let max_size = size(
5152 (120. * em_width) // Default size
5153 .min(hitbox.size.width / 2.) // Shrink to half of the editor width
5154 .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
5155 (16. * line_height) // Default size
5156 .min(hitbox.size.height / 2.) // Shrink to half of the editor height
5157 .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
5158 );
5159
5160 let hover_popovers = self.editor.update(cx, |editor, cx| {
5161 editor.hover_state.render(
5162 snapshot,
5163 visible_display_row_range.clone(),
5164 max_size,
5165 &editor.text_layout_details(window),
5166 window,
5167 cx,
5168 )
5169 });
5170 let Some((popover_position, hover_popovers)) = hover_popovers else {
5171 return;
5172 };
5173
5174 // This is safe because we check on layout whether the required row is available
5175 let hovered_row_layout = &line_layouts[popover_position
5176 .row()
5177 .minus(visible_display_row_range.start)
5178 as usize];
5179
5180 // Compute Hovered Point
5181 let x = hovered_row_layout.x_for_index(popover_position.column() as usize)
5182 - Pixels::from(scroll_pixel_position.x);
5183 let y = Pixels::from(
5184 popover_position.row().as_f64() * ScrollPixelOffset::from(line_height)
5185 - scroll_pixel_position.y,
5186 );
5187 let hovered_point = content_origin + point(x, y);
5188
5189 let mut overall_height = Pixels::ZERO;
5190 let mut measured_hover_popovers = Vec::new();
5191 for (position, mut hover_popover) in hover_popovers.into_iter().with_position() {
5192 let size = hover_popover.layout_as_root(AvailableSpace::min_size(), window, cx);
5193 let horizontal_offset =
5194 (hitbox.top_right().x - POPOVER_RIGHT_OFFSET - (hovered_point.x + size.width))
5195 .min(Pixels::ZERO);
5196 match position {
5197 itertools::Position::Middle | itertools::Position::Last => {
5198 overall_height += HOVER_POPOVER_GAP
5199 }
5200 _ => {}
5201 }
5202 overall_height += size.height;
5203 measured_hover_popovers.push(MeasuredHoverPopover {
5204 element: hover_popover,
5205 size,
5206 horizontal_offset,
5207 });
5208 }
5209
5210 fn draw_occluder(
5211 width: Pixels,
5212 origin: gpui::Point<Pixels>,
5213 window: &mut Window,
5214 cx: &mut App,
5215 ) {
5216 let mut occlusion = div()
5217 .size_full()
5218 .occlude()
5219 .on_mouse_move(|_, _, cx| cx.stop_propagation())
5220 .into_any_element();
5221 occlusion.layout_as_root(size(width, HOVER_POPOVER_GAP).into(), window, cx);
5222 window.defer_draw(occlusion, origin, 2);
5223 }
5224
5225 fn place_popovers_above(
5226 hovered_point: gpui::Point<Pixels>,
5227 measured_hover_popovers: Vec<MeasuredHoverPopover>,
5228 window: &mut Window,
5229 cx: &mut App,
5230 ) {
5231 let mut current_y = hovered_point.y;
5232 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
5233 let size = popover.size;
5234 let popover_origin = point(
5235 hovered_point.x + popover.horizontal_offset,
5236 current_y - size.height,
5237 );
5238
5239 window.defer_draw(popover.element, popover_origin, 2);
5240 if position != itertools::Position::Last {
5241 let origin = point(popover_origin.x, popover_origin.y - HOVER_POPOVER_GAP);
5242 draw_occluder(size.width, origin, window, cx);
5243 }
5244
5245 current_y = popover_origin.y - HOVER_POPOVER_GAP;
5246 }
5247 }
5248
5249 fn place_popovers_below(
5250 hovered_point: gpui::Point<Pixels>,
5251 measured_hover_popovers: Vec<MeasuredHoverPopover>,
5252 line_height: Pixels,
5253 window: &mut Window,
5254 cx: &mut App,
5255 ) {
5256 let mut current_y = hovered_point.y + line_height;
5257 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
5258 let size = popover.size;
5259 let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
5260
5261 window.defer_draw(popover.element, popover_origin, 2);
5262 if position != itertools::Position::Last {
5263 let origin = point(popover_origin.x, popover_origin.y + size.height);
5264 draw_occluder(size.width, origin, window, cx);
5265 }
5266
5267 current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
5268 }
5269 }
5270
5271 let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
5272 context_menu_layout
5273 .as_ref()
5274 .is_some_and(|menu| bounds.intersects(&menu.bounds))
5275 };
5276
5277 let can_place_above = {
5278 let mut bounds_above = Vec::new();
5279 let mut current_y = hovered_point.y;
5280 for popover in &measured_hover_popovers {
5281 let size = popover.size;
5282 let popover_origin = point(
5283 hovered_point.x + popover.horizontal_offset,
5284 current_y - size.height,
5285 );
5286 bounds_above.push(Bounds::new(popover_origin, size));
5287 current_y = popover_origin.y - HOVER_POPOVER_GAP;
5288 }
5289 bounds_above
5290 .iter()
5291 .all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
5292 };
5293
5294 let can_place_below = || {
5295 let mut bounds_below = Vec::new();
5296 let mut current_y = hovered_point.y + line_height;
5297 for popover in &measured_hover_popovers {
5298 let size = popover.size;
5299 let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
5300 bounds_below.push(Bounds::new(popover_origin, size));
5301 current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
5302 }
5303 bounds_below
5304 .iter()
5305 .all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
5306 };
5307
5308 if can_place_above {
5309 // try placing above hovered point
5310 place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
5311 } else if can_place_below() {
5312 // try placing below hovered point
5313 place_popovers_below(
5314 hovered_point,
5315 measured_hover_popovers,
5316 line_height,
5317 window,
5318 cx,
5319 );
5320 } else {
5321 // try to place popovers around the context menu
5322 let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
5323 let total_width = measured_hover_popovers
5324 .iter()
5325 .map(|p| p.size.width)
5326 .max()
5327 .unwrap_or(Pixels::ZERO);
5328 let y_for_horizontal_positioning = if menu.y_flipped {
5329 menu.bounds.bottom() - overall_height
5330 } else {
5331 menu.bounds.top()
5332 };
5333 let possible_origins = vec![
5334 // left of context menu
5335 point(
5336 menu.bounds.left() - total_width - HOVER_POPOVER_GAP,
5337 y_for_horizontal_positioning,
5338 ),
5339 // right of context menu
5340 point(
5341 menu.bounds.right() + HOVER_POPOVER_GAP,
5342 y_for_horizontal_positioning,
5343 ),
5344 // top of context menu
5345 point(
5346 menu.bounds.left(),
5347 menu.bounds.top() - overall_height - HOVER_POPOVER_GAP,
5348 ),
5349 // bottom of context menu
5350 point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
5351 ];
5352 possible_origins.into_iter().find(|&origin| {
5353 Bounds::new(origin, size(total_width, overall_height))
5354 .is_contained_within(hitbox)
5355 })
5356 });
5357 if let Some(origin) = origin_surrounding_menu {
5358 let mut current_y = origin.y;
5359 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
5360 let size = popover.size;
5361 let popover_origin = point(origin.x, current_y);
5362
5363 window.defer_draw(popover.element, popover_origin, 2);
5364 if position != itertools::Position::Last {
5365 let origin = point(popover_origin.x, popover_origin.y + size.height);
5366 draw_occluder(size.width, origin, window, cx);
5367 }
5368
5369 current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
5370 }
5371 } else {
5372 // fallback to existing above/below cursor logic
5373 // this might overlap menu or overflow in rare case
5374 if can_place_above {
5375 place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
5376 } else {
5377 place_popovers_below(
5378 hovered_point,
5379 measured_hover_popovers,
5380 line_height,
5381 window,
5382 cx,
5383 );
5384 }
5385 }
5386 }
5387 }
5388
5389 fn layout_diff_hunk_controls(
5390 &self,
5391 row_range: Range<DisplayRow>,
5392 row_infos: &[RowInfo],
5393 text_hitbox: &Hitbox,
5394 newest_cursor_position: Option<DisplayPoint>,
5395 line_height: Pixels,
5396 right_margin: Pixels,
5397 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
5398 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
5399 highlighted_rows: &BTreeMap<DisplayRow, LineHighlight>,
5400 editor: Entity<Editor>,
5401 window: &mut Window,
5402 cx: &mut App,
5403 ) -> (Vec<AnyElement>, Vec<(DisplayRow, Bounds<Pixels>)>) {
5404 let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone();
5405 let hovered_diff_hunk_row = editor.read(cx).hovered_diff_hunk_row;
5406
5407 let mut controls = vec![];
5408 let mut control_bounds = vec![];
5409
5410 let active_positions = [
5411 hovered_diff_hunk_row.map(|row| DisplayPoint::new(row, 0)),
5412 newest_cursor_position,
5413 ];
5414
5415 for (hunk, _) in display_hunks {
5416 if let DisplayDiffHunk::Unfolded {
5417 display_row_range,
5418 multi_buffer_range,
5419 status,
5420 is_created_file,
5421 ..
5422 } = &hunk
5423 {
5424 if display_row_range.start < row_range.start
5425 || display_row_range.start >= row_range.end
5426 {
5427 continue;
5428 }
5429 if highlighted_rows
5430 .get(&display_row_range.start)
5431 .and_then(|highlight| highlight.type_id)
5432 .is_some_and(|type_id| {
5433 [
5434 TypeId::of::<ConflictsOuter>(),
5435 TypeId::of::<ConflictsOursMarker>(),
5436 TypeId::of::<ConflictsOurs>(),
5437 TypeId::of::<ConflictsTheirs>(),
5438 TypeId::of::<ConflictsTheirsMarker>(),
5439 ]
5440 .contains(&type_id)
5441 })
5442 {
5443 continue;
5444 }
5445 let row_ix = (display_row_range.start - row_range.start).0 as usize;
5446 if row_infos[row_ix].diff_status.is_none() {
5447 continue;
5448 }
5449 if row_infos[row_ix]
5450 .diff_status
5451 .is_some_and(|status| status.is_added())
5452 && !status.is_added()
5453 {
5454 continue;
5455 }
5456
5457 if active_positions
5458 .iter()
5459 .any(|p| p.is_some_and(|p| display_row_range.contains(&p.row())))
5460 {
5461 let y = (display_row_range.start.as_f64()
5462 * ScrollPixelOffset::from(line_height)
5463 + ScrollPixelOffset::from(text_hitbox.bounds.top())
5464 - scroll_pixel_position.y)
5465 .into();
5466
5467 let mut element = render_diff_hunk_controls(
5468 display_row_range.start.0,
5469 status,
5470 multi_buffer_range.clone(),
5471 *is_created_file,
5472 line_height,
5473 &editor,
5474 window,
5475 cx,
5476 );
5477 let size =
5478 element.layout_as_root(size(px(100.0), line_height).into(), window, cx);
5479
5480 let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width;
5481
5482 let bounds = Bounds::new(gpui::Point::new(x, y), size);
5483 control_bounds.push((display_row_range.start, bounds));
5484
5485 window.with_absolute_element_offset(gpui::Point::new(x, y), |window| {
5486 element.prepaint(window, cx)
5487 });
5488 controls.push(element);
5489 }
5490 }
5491 }
5492
5493 (controls, control_bounds)
5494 }
5495
5496 fn layout_signature_help(
5497 &self,
5498 hitbox: &Hitbox,
5499 content_origin: gpui::Point<Pixels>,
5500 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
5501 newest_selection_head: Option<DisplayPoint>,
5502 start_row: DisplayRow,
5503 line_layouts: &[LineWithInvisibles],
5504 line_height: Pixels,
5505 em_width: Pixels,
5506 context_menu_layout: Option<ContextMenuLayout>,
5507 window: &mut Window,
5508 cx: &mut App,
5509 ) {
5510 if !self.editor.focus_handle(cx).is_focused(window) {
5511 return;
5512 }
5513 let Some(newest_selection_head) = newest_selection_head else {
5514 return;
5515 };
5516
5517 let max_size = size(
5518 (120. * em_width) // Default size
5519 .min(hitbox.size.width / 2.) // Shrink to half of the editor width
5520 .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
5521 (16. * line_height) // Default size
5522 .min(hitbox.size.height / 2.) // Shrink to half of the editor height
5523 .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
5524 );
5525
5526 let maybe_element = self.editor.update(cx, |editor, cx| {
5527 if let Some(popover) = editor.signature_help_state.popover_mut() {
5528 let element = popover.render(max_size, window, cx);
5529 Some(element)
5530 } else {
5531 None
5532 }
5533 });
5534 let Some(mut element) = maybe_element else {
5535 return;
5536 };
5537
5538 let selection_row = newest_selection_head.row();
5539 let Some(cursor_row_layout) = (selection_row >= start_row)
5540 .then(|| line_layouts.get(selection_row.minus(start_row) as usize))
5541 .flatten()
5542 else {
5543 return;
5544 };
5545
5546 let target_x = cursor_row_layout.x_for_index(newest_selection_head.column() as usize)
5547 - Pixels::from(scroll_pixel_position.x);
5548 let target_y = Pixels::from(
5549 selection_row.as_f64() * ScrollPixelOffset::from(line_height) - scroll_pixel_position.y,
5550 );
5551 let target_point = content_origin + point(target_x, target_y);
5552
5553 let actual_size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
5554
5555 let (popover_bounds_above, popover_bounds_below) = {
5556 let horizontal_offset = (hitbox.top_right().x
5557 - POPOVER_RIGHT_OFFSET
5558 - (target_point.x + actual_size.width))
5559 .min(Pixels::ZERO);
5560 let initial_x = target_point.x + horizontal_offset;
5561 (
5562 Bounds::new(
5563 point(initial_x, target_point.y - actual_size.height),
5564 actual_size,
5565 ),
5566 Bounds::new(
5567 point(initial_x, target_point.y + line_height + HOVER_POPOVER_GAP),
5568 actual_size,
5569 ),
5570 )
5571 };
5572
5573 let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
5574 context_menu_layout
5575 .as_ref()
5576 .is_some_and(|menu| bounds.intersects(&menu.bounds))
5577 };
5578
5579 let final_origin = if popover_bounds_above.is_contained_within(hitbox)
5580 && !intersects_menu(popover_bounds_above)
5581 {
5582 // try placing above cursor
5583 popover_bounds_above.origin
5584 } else if popover_bounds_below.is_contained_within(hitbox)
5585 && !intersects_menu(popover_bounds_below)
5586 {
5587 // try placing below cursor
5588 popover_bounds_below.origin
5589 } else {
5590 // try surrounding context menu if exists
5591 let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
5592 let y_for_horizontal_positioning = if menu.y_flipped {
5593 menu.bounds.bottom() - actual_size.height
5594 } else {
5595 menu.bounds.top()
5596 };
5597 let possible_origins = vec![
5598 // left of context menu
5599 point(
5600 menu.bounds.left() - actual_size.width - HOVER_POPOVER_GAP,
5601 y_for_horizontal_positioning,
5602 ),
5603 // right of context menu
5604 point(
5605 menu.bounds.right() + HOVER_POPOVER_GAP,
5606 y_for_horizontal_positioning,
5607 ),
5608 // top of context menu
5609 point(
5610 menu.bounds.left(),
5611 menu.bounds.top() - actual_size.height - HOVER_POPOVER_GAP,
5612 ),
5613 // bottom of context menu
5614 point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
5615 ];
5616 possible_origins
5617 .into_iter()
5618 .find(|&origin| Bounds::new(origin, actual_size).is_contained_within(hitbox))
5619 });
5620 origin_surrounding_menu.unwrap_or_else(|| {
5621 // fallback to existing above/below cursor logic
5622 // this might overlap menu or overflow in rare case
5623 if popover_bounds_above.is_contained_within(hitbox) {
5624 popover_bounds_above.origin
5625 } else {
5626 popover_bounds_below.origin
5627 }
5628 })
5629 };
5630
5631 window.defer_draw(element, final_origin, 2);
5632 }
5633
5634 fn paint_background(&self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
5635 window.paint_layer(layout.hitbox.bounds, |window| {
5636 let scroll_top = layout.position_map.snapshot.scroll_position().y;
5637 let gutter_bg = cx.theme().colors().editor_gutter_background;
5638 window.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg));
5639 window.paint_quad(fill(
5640 layout.position_map.text_hitbox.bounds,
5641 self.style.background,
5642 ));
5643
5644 if matches!(
5645 layout.mode,
5646 EditorMode::Full { .. } | EditorMode::Minimap { .. }
5647 ) {
5648 let show_active_line_background = match layout.mode {
5649 EditorMode::Full {
5650 show_active_line_background,
5651 ..
5652 } => show_active_line_background,
5653 EditorMode::Minimap { .. } => true,
5654 _ => false,
5655 };
5656 let mut active_rows = layout.active_rows.iter().peekable();
5657 while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
5658 let mut end_row = start_row.0;
5659 while active_rows
5660 .peek()
5661 .is_some_and(|(active_row, has_selection)| {
5662 active_row.0 == end_row + 1
5663 && has_selection.selection == contains_non_empty_selection.selection
5664 })
5665 {
5666 active_rows.next().unwrap();
5667 end_row += 1;
5668 }
5669
5670 if show_active_line_background && !contains_non_empty_selection.selection {
5671 let highlight_h_range =
5672 match layout.position_map.snapshot.current_line_highlight {
5673 CurrentLineHighlight::Gutter => Some(Range {
5674 start: layout.hitbox.left(),
5675 end: layout.gutter_hitbox.right(),
5676 }),
5677 CurrentLineHighlight::Line => Some(Range {
5678 start: layout.position_map.text_hitbox.bounds.left(),
5679 end: layout.position_map.text_hitbox.bounds.right(),
5680 }),
5681 CurrentLineHighlight::All => Some(Range {
5682 start: layout.hitbox.left(),
5683 end: layout.hitbox.right(),
5684 }),
5685 CurrentLineHighlight::None => None,
5686 };
5687 if let Some(range) = highlight_h_range {
5688 let active_line_bg = cx.theme().colors().editor_active_line_background;
5689 let bounds = Bounds {
5690 origin: point(
5691 range.start,
5692 layout.hitbox.origin.y
5693 + Pixels::from(
5694 (start_row.as_f64() - scroll_top)
5695 * ScrollPixelOffset::from(
5696 layout.position_map.line_height,
5697 ),
5698 ),
5699 ),
5700 size: size(
5701 range.end - range.start,
5702 layout.position_map.line_height
5703 * (end_row - start_row.0 + 1) as f32,
5704 ),
5705 };
5706 window.paint_quad(fill(bounds, active_line_bg));
5707 }
5708 }
5709 }
5710
5711 let mut paint_highlight = |highlight_row_start: DisplayRow,
5712 highlight_row_end: DisplayRow,
5713 highlight: crate::LineHighlight,
5714 edges| {
5715 let mut origin_x = layout.hitbox.left();
5716 let mut width = layout.hitbox.size.width;
5717 if !highlight.include_gutter {
5718 origin_x += layout.gutter_hitbox.size.width;
5719 width -= layout.gutter_hitbox.size.width;
5720 }
5721
5722 let origin = point(
5723 origin_x,
5724 layout.hitbox.origin.y
5725 + Pixels::from(
5726 (highlight_row_start.as_f64() - scroll_top)
5727 * ScrollPixelOffset::from(layout.position_map.line_height),
5728 ),
5729 );
5730 let size = size(
5731 width,
5732 layout.position_map.line_height
5733 * highlight_row_end.next_row().minus(highlight_row_start) as f32,
5734 );
5735 let mut quad = fill(Bounds { origin, size }, highlight.background);
5736 if let Some(border_color) = highlight.border {
5737 quad.border_color = border_color;
5738 quad.border_widths = edges
5739 }
5740 window.paint_quad(quad);
5741 };
5742
5743 let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
5744 None;
5745 for (&new_row, &new_background) in &layout.highlighted_rows {
5746 match &mut current_paint {
5747 &mut Some((current_background, ref mut current_range, mut edges)) => {
5748 let new_range_started = current_background != new_background
5749 || current_range.end.next_row() != new_row;
5750 if new_range_started {
5751 if current_range.end.next_row() == new_row {
5752 edges.bottom = px(0.);
5753 };
5754 paint_highlight(
5755 current_range.start,
5756 current_range.end,
5757 current_background,
5758 edges,
5759 );
5760 let edges = Edges {
5761 top: if current_range.end.next_row() != new_row {
5762 px(1.)
5763 } else {
5764 px(0.)
5765 },
5766 bottom: px(1.),
5767 ..Default::default()
5768 };
5769 current_paint = Some((new_background, new_row..new_row, edges));
5770 continue;
5771 } else {
5772 current_range.end = current_range.end.next_row();
5773 }
5774 }
5775 None => {
5776 let edges = Edges {
5777 top: px(1.),
5778 bottom: px(1.),
5779 ..Default::default()
5780 };
5781 current_paint = Some((new_background, new_row..new_row, edges))
5782 }
5783 };
5784 }
5785 if let Some((color, range, edges)) = current_paint {
5786 paint_highlight(range.start, range.end, color, edges);
5787 }
5788
5789 for (guide_x, active) in layout.wrap_guides.iter() {
5790 let color = if *active {
5791 cx.theme().colors().editor_active_wrap_guide
5792 } else {
5793 cx.theme().colors().editor_wrap_guide
5794 };
5795 window.paint_quad(fill(
5796 Bounds {
5797 origin: point(*guide_x, layout.position_map.text_hitbox.origin.y),
5798 size: size(px(1.), layout.position_map.text_hitbox.size.height),
5799 },
5800 color,
5801 ));
5802 }
5803 }
5804 })
5805 }
5806
5807 fn paint_indent_guides(
5808 &mut self,
5809 layout: &mut EditorLayout,
5810 window: &mut Window,
5811 cx: &mut App,
5812 ) {
5813 let Some(indent_guides) = &layout.indent_guides else {
5814 return;
5815 };
5816
5817 let faded_color = |color: Hsla, alpha: f32| {
5818 let mut faded = color;
5819 faded.a = alpha;
5820 faded
5821 };
5822
5823 for indent_guide in indent_guides {
5824 let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
5825 let settings = &indent_guide.settings;
5826
5827 // TODO fixed for now, expose them through themes later
5828 const INDENT_AWARE_ALPHA: f32 = 0.2;
5829 const INDENT_AWARE_ACTIVE_ALPHA: f32 = 0.4;
5830 const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
5831 const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
5832
5833 let line_color = match (settings.coloring, indent_guide.active) {
5834 (IndentGuideColoring::Disabled, _) => None,
5835 (IndentGuideColoring::Fixed, false) => {
5836 Some(cx.theme().colors().editor_indent_guide)
5837 }
5838 (IndentGuideColoring::Fixed, true) => {
5839 Some(cx.theme().colors().editor_indent_guide_active)
5840 }
5841 (IndentGuideColoring::IndentAware, false) => {
5842 Some(faded_color(indent_accent_colors, INDENT_AWARE_ALPHA))
5843 }
5844 (IndentGuideColoring::IndentAware, true) => {
5845 Some(faded_color(indent_accent_colors, INDENT_AWARE_ACTIVE_ALPHA))
5846 }
5847 };
5848
5849 let background_color = match (settings.background_coloring, indent_guide.active) {
5850 (IndentGuideBackgroundColoring::Disabled, _) => None,
5851 (IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
5852 indent_accent_colors,
5853 INDENT_AWARE_BACKGROUND_ALPHA,
5854 )),
5855 (IndentGuideBackgroundColoring::IndentAware, true) => Some(faded_color(
5856 indent_accent_colors,
5857 INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA,
5858 )),
5859 };
5860
5861 let requested_line_width = if indent_guide.active {
5862 settings.active_line_width
5863 } else {
5864 settings.line_width
5865 }
5866 .clamp(1, 10);
5867 let mut line_indicator_width = 0.;
5868 if let Some(color) = line_color {
5869 window.paint_quad(fill(
5870 Bounds {
5871 origin: indent_guide.origin,
5872 size: size(px(requested_line_width as f32), indent_guide.length),
5873 },
5874 color,
5875 ));
5876 line_indicator_width = requested_line_width as f32;
5877 }
5878
5879 if let Some(color) = background_color {
5880 let width = indent_guide.single_indent_width - px(line_indicator_width);
5881 window.paint_quad(fill(
5882 Bounds {
5883 origin: point(
5884 indent_guide.origin.x + px(line_indicator_width),
5885 indent_guide.origin.y,
5886 ),
5887 size: size(width, indent_guide.length),
5888 },
5889 color,
5890 ));
5891 }
5892 }
5893 }
5894
5895 fn paint_line_numbers(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
5896 let is_singleton = self.editor.read(cx).buffer_kind(cx) == ItemBufferKind::Singleton;
5897
5898 let line_height = layout.position_map.line_height;
5899 window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
5900
5901 for line_layout in layout.line_numbers.values() {
5902 for LineNumberSegment {
5903 shaped_line,
5904 hitbox,
5905 } in &line_layout.segments
5906 {
5907 let Some(hitbox) = hitbox else {
5908 continue;
5909 };
5910
5911 let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
5912 let color = cx.theme().colors().editor_hover_line_number;
5913
5914 let line = self.shape_line_number(shaped_line.text.clone(), color, window);
5915 line.paint(hitbox.origin, line_height, window, cx).log_err()
5916 } else {
5917 shaped_line
5918 .paint(hitbox.origin, line_height, window, cx)
5919 .log_err()
5920 }) else {
5921 continue;
5922 };
5923
5924 // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
5925 // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
5926 if is_singleton {
5927 window.set_cursor_style(CursorStyle::IBeam, hitbox);
5928 } else {
5929 window.set_cursor_style(CursorStyle::PointingHand, hitbox);
5930 }
5931 }
5932 }
5933 }
5934
5935 fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
5936 if layout.display_hunks.is_empty() {
5937 return;
5938 }
5939
5940 let line_height = layout.position_map.line_height;
5941 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
5942 for (hunk, hitbox) in &layout.display_hunks {
5943 let hunk_to_paint = match hunk {
5944 DisplayDiffHunk::Folded { .. } => {
5945 let hunk_bounds = Self::diff_hunk_bounds(
5946 &layout.position_map.snapshot,
5947 line_height,
5948 layout.gutter_hitbox.bounds,
5949 hunk,
5950 );
5951 Some((
5952 hunk_bounds,
5953 cx.theme().colors().version_control_modified,
5954 Corners::all(px(0.)),
5955 DiffHunkStatus::modified_none(),
5956 ))
5957 }
5958 DisplayDiffHunk::Unfolded {
5959 status,
5960 display_row_range,
5961 ..
5962 } => hitbox.as_ref().map(|hunk_hitbox| match status.kind {
5963 DiffHunkStatusKind::Added => (
5964 hunk_hitbox.bounds,
5965 cx.theme().colors().version_control_added,
5966 Corners::all(px(0.)),
5967 *status,
5968 ),
5969 DiffHunkStatusKind::Modified => (
5970 hunk_hitbox.bounds,
5971 cx.theme().colors().version_control_modified,
5972 Corners::all(px(0.)),
5973 *status,
5974 ),
5975 DiffHunkStatusKind::Deleted if !display_row_range.is_empty() => (
5976 hunk_hitbox.bounds,
5977 cx.theme().colors().version_control_deleted,
5978 Corners::all(px(0.)),
5979 *status,
5980 ),
5981 DiffHunkStatusKind::Deleted => (
5982 Bounds::new(
5983 point(
5984 hunk_hitbox.origin.x - hunk_hitbox.size.width,
5985 hunk_hitbox.origin.y,
5986 ),
5987 size(hunk_hitbox.size.width * 2., hunk_hitbox.size.height),
5988 ),
5989 cx.theme().colors().version_control_deleted,
5990 Corners::all(1. * line_height),
5991 *status,
5992 ),
5993 }),
5994 };
5995
5996 if let Some((hunk_bounds, background_color, corner_radii, status)) = hunk_to_paint {
5997 // Flatten the background color with the editor color to prevent
5998 // elements below transparent hunks from showing through
5999 let flattened_background_color = cx
6000 .theme()
6001 .colors()
6002 .editor_background
6003 .blend(background_color);
6004
6005 if !Self::diff_hunk_hollow(status, cx) {
6006 window.paint_quad(quad(
6007 hunk_bounds,
6008 corner_radii,
6009 flattened_background_color,
6010 Edges::default(),
6011 transparent_black(),
6012 BorderStyle::default(),
6013 ));
6014 } else {
6015 let flattened_unstaged_background_color = cx
6016 .theme()
6017 .colors()
6018 .editor_background
6019 .blend(background_color.opacity(0.3));
6020
6021 window.paint_quad(quad(
6022 hunk_bounds,
6023 corner_radii,
6024 flattened_unstaged_background_color,
6025 Edges::all(px(1.0)),
6026 flattened_background_color,
6027 BorderStyle::Solid,
6028 ));
6029 }
6030 }
6031 }
6032 });
6033 }
6034
6035 fn gutter_strip_width(line_height: Pixels) -> Pixels {
6036 (0.275 * line_height).floor()
6037 }
6038
6039 fn diff_hunk_bounds(
6040 snapshot: &EditorSnapshot,
6041 line_height: Pixels,
6042 gutter_bounds: Bounds<Pixels>,
6043 hunk: &DisplayDiffHunk,
6044 ) -> Bounds<Pixels> {
6045 let scroll_position = snapshot.scroll_position();
6046 let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
6047 let gutter_strip_width = Self::gutter_strip_width(line_height);
6048
6049 match hunk {
6050 DisplayDiffHunk::Folded { display_row, .. } => {
6051 let start_y = (display_row.as_f64() * ScrollPixelOffset::from(line_height)
6052 - scroll_top)
6053 .into();
6054 let end_y = start_y + line_height;
6055 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
6056 let highlight_size = size(gutter_strip_width, end_y - start_y);
6057 Bounds::new(highlight_origin, highlight_size)
6058 }
6059 DisplayDiffHunk::Unfolded {
6060 display_row_range,
6061 status,
6062 ..
6063 } => {
6064 if status.is_deleted() && display_row_range.is_empty() {
6065 let row = display_row_range.start;
6066
6067 let offset = ScrollPixelOffset::from(line_height / 2.);
6068 let start_y =
6069 (row.as_f64() * ScrollPixelOffset::from(line_height) - offset - scroll_top)
6070 .into();
6071 let end_y = start_y + line_height;
6072
6073 let width = (0.35 * line_height).floor();
6074 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
6075 let highlight_size = size(width, end_y - start_y);
6076 Bounds::new(highlight_origin, highlight_size)
6077 } else {
6078 let start_row = display_row_range.start;
6079 let end_row = display_row_range.end;
6080 // If we're in a multibuffer, row range span might include an
6081 // excerpt header, so if we were to draw the marker straight away,
6082 // the hunk might include the rows of that header.
6083 // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
6084 // Instead, we simply check whether the range we're dealing with includes
6085 // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header.
6086 let end_row_in_current_excerpt = snapshot
6087 .blocks_in_range(start_row..end_row)
6088 .find_map(|(start_row, block)| {
6089 if matches!(
6090 block,
6091 Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
6092 ) {
6093 Some(start_row)
6094 } else {
6095 None
6096 }
6097 })
6098 .unwrap_or(end_row);
6099
6100 let start_y = (start_row.as_f64() * ScrollPixelOffset::from(line_height)
6101 - scroll_top)
6102 .into();
6103 let end_y = Pixels::from(
6104 end_row_in_current_excerpt.as_f64() * ScrollPixelOffset::from(line_height)
6105 - scroll_top,
6106 );
6107
6108 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
6109 let highlight_size = size(gutter_strip_width, end_y - start_y);
6110 Bounds::new(highlight_origin, highlight_size)
6111 }
6112 }
6113 }
6114 }
6115
6116 fn paint_gutter_indicators(
6117 &self,
6118 layout: &mut EditorLayout,
6119 window: &mut Window,
6120 cx: &mut App,
6121 ) {
6122 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
6123 window.with_element_namespace("crease_toggles", |window| {
6124 for crease_toggle in layout.crease_toggles.iter_mut().flatten() {
6125 crease_toggle.paint(window, cx);
6126 }
6127 });
6128
6129 window.with_element_namespace("expand_toggles", |window| {
6130 for (expand_toggle, _) in layout.expand_toggles.iter_mut().flatten() {
6131 expand_toggle.paint(window, cx);
6132 }
6133 });
6134
6135 for breakpoint in layout.breakpoints.iter_mut() {
6136 breakpoint.paint(window, cx);
6137 }
6138
6139 for test_indicator in layout.test_indicators.iter_mut() {
6140 test_indicator.paint(window, cx);
6141 }
6142 });
6143 }
6144
6145 fn paint_gutter_highlights(
6146 &self,
6147 layout: &mut EditorLayout,
6148 window: &mut Window,
6149 cx: &mut App,
6150 ) {
6151 for (_, hunk_hitbox) in &layout.display_hunks {
6152 if let Some(hunk_hitbox) = hunk_hitbox
6153 && !self
6154 .editor
6155 .read(cx)
6156 .buffer()
6157 .read(cx)
6158 .all_diff_hunks_expanded()
6159 {
6160 window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
6161 }
6162 }
6163
6164 let show_git_gutter = layout
6165 .position_map
6166 .snapshot
6167 .show_git_diff_gutter
6168 .unwrap_or_else(|| {
6169 matches!(
6170 ProjectSettings::get_global(cx).git.git_gutter,
6171 GitGutterSetting::TrackedFiles
6172 )
6173 });
6174 if show_git_gutter {
6175 Self::paint_gutter_diff_hunks(layout, window, cx)
6176 }
6177
6178 let highlight_width = 0.275 * layout.position_map.line_height;
6179 let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
6180 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
6181 for (range, color) in &layout.highlighted_gutter_ranges {
6182 let start_row = if range.start.row() < layout.visible_display_row_range.start {
6183 layout.visible_display_row_range.start - DisplayRow(1)
6184 } else {
6185 range.start.row()
6186 };
6187 let end_row = if range.end.row() > layout.visible_display_row_range.end {
6188 layout.visible_display_row_range.end + DisplayRow(1)
6189 } else {
6190 range.end.row()
6191 };
6192
6193 let start_y = layout.gutter_hitbox.top()
6194 + Pixels::from(
6195 start_row.0 as f64
6196 * ScrollPixelOffset::from(layout.position_map.line_height)
6197 - layout.position_map.scroll_pixel_position.y,
6198 );
6199 let end_y = layout.gutter_hitbox.top()
6200 + Pixels::from(
6201 (end_row.0 + 1) as f64
6202 * ScrollPixelOffset::from(layout.position_map.line_height)
6203 - layout.position_map.scroll_pixel_position.y,
6204 );
6205 let bounds = Bounds::from_corners(
6206 point(layout.gutter_hitbox.left(), start_y),
6207 point(layout.gutter_hitbox.left() + highlight_width, end_y),
6208 );
6209 window.paint_quad(fill(bounds, *color).corner_radii(highlight_corner_radii));
6210 }
6211 });
6212 }
6213
6214 fn paint_blamed_display_rows(
6215 &self,
6216 layout: &mut EditorLayout,
6217 window: &mut Window,
6218 cx: &mut App,
6219 ) {
6220 let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
6221 return;
6222 };
6223
6224 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
6225 for mut blame_element in blamed_display_rows.into_iter() {
6226 blame_element.paint(window, cx);
6227 }
6228 })
6229 }
6230
6231 fn paint_text(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
6232 window.with_content_mask(
6233 Some(ContentMask {
6234 bounds: layout.position_map.text_hitbox.bounds,
6235 }),
6236 |window| {
6237 let editor = self.editor.read(cx);
6238 if editor.mouse_cursor_hidden {
6239 window.set_window_cursor_style(CursorStyle::None);
6240 } else if let SelectionDragState::ReadyToDrag {
6241 mouse_down_time, ..
6242 } = &editor.selection_drag_state
6243 {
6244 let drag_and_drop_delay = Duration::from_millis(
6245 EditorSettings::get_global(cx)
6246 .drag_and_drop_selection
6247 .delay
6248 .0,
6249 );
6250 if mouse_down_time.elapsed() >= drag_and_drop_delay {
6251 window.set_cursor_style(
6252 CursorStyle::DragCopy,
6253 &layout.position_map.text_hitbox,
6254 );
6255 }
6256 } else if matches!(
6257 editor.selection_drag_state,
6258 SelectionDragState::Dragging { .. }
6259 ) {
6260 window
6261 .set_cursor_style(CursorStyle::DragCopy, &layout.position_map.text_hitbox);
6262 } else if editor
6263 .hovered_link_state
6264 .as_ref()
6265 .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
6266 {
6267 window.set_cursor_style(
6268 CursorStyle::PointingHand,
6269 &layout.position_map.text_hitbox,
6270 );
6271 } else {
6272 window.set_cursor_style(CursorStyle::IBeam, &layout.position_map.text_hitbox);
6273 };
6274
6275 self.paint_lines_background(layout, window, cx);
6276 let invisible_display_ranges = self.paint_highlights(layout, window, cx);
6277 self.paint_document_colors(layout, window);
6278 self.paint_lines(&invisible_display_ranges, layout, window, cx);
6279 self.paint_redactions(layout, window);
6280 self.paint_cursors(layout, window, cx);
6281 self.paint_inline_diagnostics(layout, window, cx);
6282 self.paint_inline_blame(layout, window, cx);
6283 self.paint_inline_code_actions(layout, window, cx);
6284 self.paint_diff_hunk_controls(layout, window, cx);
6285 window.with_element_namespace("crease_trailers", |window| {
6286 for trailer in layout.crease_trailers.iter_mut().flatten() {
6287 trailer.element.paint(window, cx);
6288 }
6289 });
6290 },
6291 )
6292 }
6293
6294 fn paint_highlights(
6295 &mut self,
6296 layout: &mut EditorLayout,
6297 window: &mut Window,
6298 cx: &mut App,
6299 ) -> SmallVec<[Range<DisplayPoint>; 32]> {
6300 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
6301 let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
6302 let line_end_overshoot = 0.15 * layout.position_map.line_height;
6303 for (range, color) in &layout.highlighted_ranges {
6304 self.paint_highlighted_range(
6305 range.clone(),
6306 true,
6307 *color,
6308 Pixels::ZERO,
6309 line_end_overshoot,
6310 layout,
6311 window,
6312 );
6313 }
6314
6315 let corner_radius = if EditorSettings::get_global(cx).rounded_selection {
6316 0.15 * layout.position_map.line_height
6317 } else {
6318 Pixels::ZERO
6319 };
6320
6321 for (player_color, selections) in &layout.selections {
6322 for selection in selections.iter() {
6323 self.paint_highlighted_range(
6324 selection.range.clone(),
6325 true,
6326 player_color.selection,
6327 corner_radius,
6328 corner_radius * 2.,
6329 layout,
6330 window,
6331 );
6332
6333 if selection.is_local && !selection.range.is_empty() {
6334 invisible_display_ranges.push(selection.range.clone());
6335 }
6336 }
6337 }
6338 invisible_display_ranges
6339 })
6340 }
6341
6342 fn paint_lines(
6343 &mut self,
6344 invisible_display_ranges: &[Range<DisplayPoint>],
6345 layout: &mut EditorLayout,
6346 window: &mut Window,
6347 cx: &mut App,
6348 ) {
6349 let whitespace_setting = self
6350 .editor
6351 .read(cx)
6352 .buffer
6353 .read(cx)
6354 .language_settings(cx)
6355 .show_whitespaces;
6356
6357 for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
6358 let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
6359 line_with_invisibles.draw(
6360 layout,
6361 row,
6362 layout.content_origin,
6363 whitespace_setting,
6364 invisible_display_ranges,
6365 window,
6366 cx,
6367 )
6368 }
6369
6370 for line_element in &mut layout.line_elements {
6371 line_element.paint(window, cx);
6372 }
6373 }
6374
6375 fn paint_lines_background(
6376 &mut self,
6377 layout: &mut EditorLayout,
6378 window: &mut Window,
6379 cx: &mut App,
6380 ) {
6381 for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
6382 let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
6383 line_with_invisibles.draw_background(layout, row, layout.content_origin, window, cx);
6384 }
6385 }
6386
6387 fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
6388 if layout.redacted_ranges.is_empty() {
6389 return;
6390 }
6391
6392 let line_end_overshoot = layout.line_end_overshoot();
6393
6394 // A softer than perfect black
6395 let redaction_color = gpui::rgb(0x0e1111);
6396
6397 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
6398 for range in layout.redacted_ranges.iter() {
6399 self.paint_highlighted_range(
6400 range.clone(),
6401 true,
6402 redaction_color.into(),
6403 Pixels::ZERO,
6404 line_end_overshoot,
6405 layout,
6406 window,
6407 );
6408 }
6409 });
6410 }
6411
6412 fn paint_document_colors(&self, layout: &mut EditorLayout, window: &mut Window) {
6413 let Some((colors_render_mode, image_colors)) = &layout.document_colors else {
6414 return;
6415 };
6416 if image_colors.is_empty()
6417 || colors_render_mode == &DocumentColorsRenderMode::None
6418 || colors_render_mode == &DocumentColorsRenderMode::Inlay
6419 {
6420 return;
6421 }
6422
6423 let line_end_overshoot = layout.line_end_overshoot();
6424
6425 for (range, color) in image_colors {
6426 match colors_render_mode {
6427 DocumentColorsRenderMode::Inlay | DocumentColorsRenderMode::None => return,
6428 DocumentColorsRenderMode::Background => {
6429 self.paint_highlighted_range(
6430 range.clone(),
6431 true,
6432 *color,
6433 Pixels::ZERO,
6434 line_end_overshoot,
6435 layout,
6436 window,
6437 );
6438 }
6439 DocumentColorsRenderMode::Border => {
6440 self.paint_highlighted_range(
6441 range.clone(),
6442 false,
6443 *color,
6444 Pixels::ZERO,
6445 line_end_overshoot,
6446 layout,
6447 window,
6448 );
6449 }
6450 }
6451 }
6452 }
6453
6454 fn paint_cursors(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
6455 for cursor in &mut layout.visible_cursors {
6456 cursor.paint(layout.content_origin, window, cx);
6457 }
6458 }
6459
6460 fn paint_scrollbars(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
6461 let Some(scrollbars_layout) = layout.scrollbars_layout.take() else {
6462 return;
6463 };
6464 let any_scrollbar_dragged = self.editor.read(cx).scroll_manager.any_scrollbar_dragged();
6465
6466 for (scrollbar_layout, axis) in scrollbars_layout.iter_scrollbars() {
6467 let hitbox = &scrollbar_layout.hitbox;
6468 if scrollbars_layout.visible {
6469 let scrollbar_edges = match axis {
6470 ScrollbarAxis::Horizontal => Edges {
6471 top: Pixels::ZERO,
6472 right: Pixels::ZERO,
6473 bottom: Pixels::ZERO,
6474 left: Pixels::ZERO,
6475 },
6476 ScrollbarAxis::Vertical => Edges {
6477 top: Pixels::ZERO,
6478 right: Pixels::ZERO,
6479 bottom: Pixels::ZERO,
6480 left: ScrollbarLayout::BORDER_WIDTH,
6481 },
6482 };
6483
6484 window.paint_layer(hitbox.bounds, |window| {
6485 window.paint_quad(quad(
6486 hitbox.bounds,
6487 Corners::default(),
6488 cx.theme().colors().scrollbar_track_background,
6489 scrollbar_edges,
6490 cx.theme().colors().scrollbar_track_border,
6491 BorderStyle::Solid,
6492 ));
6493
6494 if axis == ScrollbarAxis::Vertical {
6495 let fast_markers =
6496 self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx);
6497 // Refresh slow scrollbar markers in the background. Below, we
6498 // paint whatever markers have already been computed.
6499 self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, window, cx);
6500
6501 let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone();
6502 for marker in markers.iter().chain(&fast_markers) {
6503 let mut marker = marker.clone();
6504 marker.bounds.origin += hitbox.origin;
6505 window.paint_quad(marker);
6506 }
6507 }
6508
6509 if let Some(thumb_bounds) = scrollbar_layout.thumb_bounds {
6510 let scrollbar_thumb_color = match scrollbar_layout.thumb_state {
6511 ScrollbarThumbState::Dragging => {
6512 cx.theme().colors().scrollbar_thumb_active_background
6513 }
6514 ScrollbarThumbState::Hovered => {
6515 cx.theme().colors().scrollbar_thumb_hover_background
6516 }
6517 ScrollbarThumbState::Idle => {
6518 cx.theme().colors().scrollbar_thumb_background
6519 }
6520 };
6521 window.paint_quad(quad(
6522 thumb_bounds,
6523 Corners::default(),
6524 scrollbar_thumb_color,
6525 scrollbar_edges,
6526 cx.theme().colors().scrollbar_thumb_border,
6527 BorderStyle::Solid,
6528 ));
6529
6530 if any_scrollbar_dragged {
6531 window.set_window_cursor_style(CursorStyle::Arrow);
6532 } else {
6533 window.set_cursor_style(CursorStyle::Arrow, hitbox);
6534 }
6535 }
6536 })
6537 }
6538 }
6539
6540 window.on_mouse_event({
6541 let editor = self.editor.clone();
6542 let scrollbars_layout = scrollbars_layout.clone();
6543
6544 let mut mouse_position = window.mouse_position();
6545 move |event: &MouseMoveEvent, phase, window, cx| {
6546 if phase == DispatchPhase::Capture {
6547 return;
6548 }
6549
6550 editor.update(cx, |editor, cx| {
6551 if let Some((scrollbar_layout, axis)) = event
6552 .pressed_button
6553 .filter(|button| *button == MouseButton::Left)
6554 .and(editor.scroll_manager.dragging_scrollbar_axis())
6555 .and_then(|axis| {
6556 scrollbars_layout
6557 .iter_scrollbars()
6558 .find(|(_, a)| *a == axis)
6559 })
6560 {
6561 let ScrollbarLayout {
6562 hitbox,
6563 text_unit_size,
6564 ..
6565 } = scrollbar_layout;
6566
6567 let old_position = mouse_position.along(axis);
6568 let new_position = event.position.along(axis);
6569 if (hitbox.origin.along(axis)..hitbox.bottom_right().along(axis))
6570 .contains(&old_position)
6571 {
6572 let position = editor.scroll_position(cx).apply_along(axis, |p| {
6573 (p + ScrollOffset::from(
6574 (new_position - old_position) / *text_unit_size,
6575 ))
6576 .max(0.)
6577 });
6578 editor.set_scroll_position(position, window, cx);
6579 }
6580
6581 editor.scroll_manager.show_scrollbars(window, cx);
6582 cx.stop_propagation();
6583 } else if let Some((layout, axis)) = scrollbars_layout
6584 .get_hovered_axis(window)
6585 .filter(|_| !event.dragging())
6586 {
6587 if layout.thumb_hovered(&event.position) {
6588 editor
6589 .scroll_manager
6590 .set_hovered_scroll_thumb_axis(axis, cx);
6591 } else {
6592 editor.scroll_manager.reset_scrollbar_state(cx);
6593 }
6594
6595 editor.scroll_manager.show_scrollbars(window, cx);
6596 } else {
6597 editor.scroll_manager.reset_scrollbar_state(cx);
6598 }
6599
6600 mouse_position = event.position;
6601 })
6602 }
6603 });
6604
6605 if any_scrollbar_dragged {
6606 window.on_mouse_event({
6607 let editor = self.editor.clone();
6608 move |_: &MouseUpEvent, phase, window, cx| {
6609 if phase == DispatchPhase::Capture {
6610 return;
6611 }
6612
6613 editor.update(cx, |editor, cx| {
6614 if let Some((_, axis)) = scrollbars_layout.get_hovered_axis(window) {
6615 editor
6616 .scroll_manager
6617 .set_hovered_scroll_thumb_axis(axis, cx);
6618 } else {
6619 editor.scroll_manager.reset_scrollbar_state(cx);
6620 }
6621 cx.stop_propagation();
6622 });
6623 }
6624 });
6625 } else {
6626 window.on_mouse_event({
6627 let editor = self.editor.clone();
6628
6629 move |event: &MouseDownEvent, phase, window, cx| {
6630 if phase == DispatchPhase::Capture {
6631 return;
6632 }
6633 let Some((scrollbar_layout, axis)) = scrollbars_layout.get_hovered_axis(window)
6634 else {
6635 return;
6636 };
6637
6638 let ScrollbarLayout {
6639 hitbox,
6640 visible_range,
6641 text_unit_size,
6642 thumb_bounds,
6643 ..
6644 } = scrollbar_layout;
6645
6646 let Some(thumb_bounds) = thumb_bounds else {
6647 return;
6648 };
6649
6650 editor.update(cx, |editor, cx| {
6651 editor
6652 .scroll_manager
6653 .set_dragged_scroll_thumb_axis(axis, cx);
6654
6655 let event_position = event.position.along(axis);
6656
6657 if event_position < thumb_bounds.origin.along(axis)
6658 || thumb_bounds.bottom_right().along(axis) < event_position
6659 {
6660 let center_position = ((event_position - hitbox.origin.along(axis))
6661 / *text_unit_size)
6662 .round() as u32;
6663 let start_position = center_position.saturating_sub(
6664 (visible_range.end - visible_range.start) as u32 / 2,
6665 );
6666
6667 let position = editor
6668 .scroll_position(cx)
6669 .apply_along(axis, |_| start_position as ScrollOffset);
6670
6671 editor.set_scroll_position(position, window, cx);
6672 } else {
6673 editor.scroll_manager.show_scrollbars(window, cx);
6674 }
6675
6676 cx.stop_propagation();
6677 });
6678 }
6679 });
6680 }
6681 }
6682
6683 fn collect_fast_scrollbar_markers(
6684 &self,
6685 layout: &EditorLayout,
6686 scrollbar_layout: &ScrollbarLayout,
6687 cx: &mut App,
6688 ) -> Vec<PaintQuad> {
6689 const LIMIT: usize = 100;
6690 if !EditorSettings::get_global(cx).scrollbar.cursors || layout.cursors.len() > LIMIT {
6691 return vec![];
6692 }
6693 let cursor_ranges = layout
6694 .cursors
6695 .iter()
6696 .map(|(point, color)| ColoredRange {
6697 start: point.row(),
6698 end: point.row(),
6699 color: *color,
6700 })
6701 .collect_vec();
6702 scrollbar_layout.marker_quads_for_ranges(cursor_ranges, None)
6703 }
6704
6705 fn refresh_slow_scrollbar_markers(
6706 &self,
6707 layout: &EditorLayout,
6708 scrollbar_layout: &ScrollbarLayout,
6709 window: &mut Window,
6710 cx: &mut App,
6711 ) {
6712 self.editor.update(cx, |editor, cx| {
6713 if editor.buffer_kind(cx) != ItemBufferKind::Singleton
6714 || !editor
6715 .scrollbar_marker_state
6716 .should_refresh(scrollbar_layout.hitbox.size)
6717 {
6718 return;
6719 }
6720
6721 let scrollbar_layout = scrollbar_layout.clone();
6722 let background_highlights = editor.background_highlights.clone();
6723 let snapshot = layout.position_map.snapshot.clone();
6724 let theme = cx.theme().clone();
6725 let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
6726
6727 editor.scrollbar_marker_state.dirty = false;
6728 editor.scrollbar_marker_state.pending_refresh =
6729 Some(cx.spawn_in(window, async move |editor, cx| {
6730 let scrollbar_size = scrollbar_layout.hitbox.size;
6731 let scrollbar_markers = cx
6732 .background_spawn(async move {
6733 let max_point = snapshot.display_snapshot.buffer_snapshot().max_point();
6734 let mut marker_quads = Vec::new();
6735 if scrollbar_settings.git_diff {
6736 let marker_row_ranges =
6737 snapshot.buffer_snapshot().diff_hunks().map(|hunk| {
6738 let start_display_row =
6739 MultiBufferPoint::new(hunk.row_range.start.0, 0)
6740 .to_display_point(&snapshot.display_snapshot)
6741 .row();
6742 let mut end_display_row =
6743 MultiBufferPoint::new(hunk.row_range.end.0, 0)
6744 .to_display_point(&snapshot.display_snapshot)
6745 .row();
6746 if end_display_row != start_display_row {
6747 end_display_row.0 -= 1;
6748 }
6749 let color = match &hunk.status().kind {
6750 DiffHunkStatusKind::Added => {
6751 theme.colors().version_control_added
6752 }
6753 DiffHunkStatusKind::Modified => {
6754 theme.colors().version_control_modified
6755 }
6756 DiffHunkStatusKind::Deleted => {
6757 theme.colors().version_control_deleted
6758 }
6759 };
6760 ColoredRange {
6761 start: start_display_row,
6762 end: end_display_row,
6763 color,
6764 }
6765 });
6766
6767 marker_quads.extend(
6768 scrollbar_layout
6769 .marker_quads_for_ranges(marker_row_ranges, Some(0)),
6770 );
6771 }
6772
6773 for (background_highlight_id, (_, background_ranges)) in
6774 background_highlights.iter()
6775 {
6776 let is_search_highlights = *background_highlight_id
6777 == HighlightKey::Type(TypeId::of::<BufferSearchHighlights>());
6778 let is_text_highlights = *background_highlight_id
6779 == HighlightKey::Type(TypeId::of::<SelectedTextHighlight>());
6780 let is_symbol_occurrences = *background_highlight_id
6781 == HighlightKey::Type(TypeId::of::<DocumentHighlightRead>())
6782 || *background_highlight_id
6783 == HighlightKey::Type(
6784 TypeId::of::<DocumentHighlightWrite>(),
6785 );
6786 if (is_search_highlights && scrollbar_settings.search_results)
6787 || (is_text_highlights && scrollbar_settings.selected_text)
6788 || (is_symbol_occurrences && scrollbar_settings.selected_symbol)
6789 {
6790 let mut color = theme.status().info;
6791 if is_symbol_occurrences {
6792 color.fade_out(0.5);
6793 }
6794 let marker_row_ranges = background_ranges.iter().map(|range| {
6795 let display_start = range
6796 .start
6797 .to_display_point(&snapshot.display_snapshot);
6798 let display_end =
6799 range.end.to_display_point(&snapshot.display_snapshot);
6800 ColoredRange {
6801 start: display_start.row(),
6802 end: display_end.row(),
6803 color,
6804 }
6805 });
6806 marker_quads.extend(
6807 scrollbar_layout
6808 .marker_quads_for_ranges(marker_row_ranges, Some(1)),
6809 );
6810 }
6811 }
6812
6813 if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
6814 let diagnostics = snapshot
6815 .buffer_snapshot()
6816 .diagnostics_in_range::<Point>(Point::zero()..max_point)
6817 // Don't show diagnostics the user doesn't care about
6818 .filter(|diagnostic| {
6819 match (
6820 scrollbar_settings.diagnostics,
6821 diagnostic.diagnostic.severity,
6822 ) {
6823 (ScrollbarDiagnostics::All, _) => true,
6824 (
6825 ScrollbarDiagnostics::Error,
6826 lsp::DiagnosticSeverity::ERROR,
6827 ) => true,
6828 (
6829 ScrollbarDiagnostics::Warning,
6830 lsp::DiagnosticSeverity::ERROR
6831 | lsp::DiagnosticSeverity::WARNING,
6832 ) => true,
6833 (
6834 ScrollbarDiagnostics::Information,
6835 lsp::DiagnosticSeverity::ERROR
6836 | lsp::DiagnosticSeverity::WARNING
6837 | lsp::DiagnosticSeverity::INFORMATION,
6838 ) => true,
6839 (_, _) => false,
6840 }
6841 })
6842 // We want to sort by severity, in order to paint the most severe diagnostics last.
6843 .sorted_by_key(|diagnostic| {
6844 std::cmp::Reverse(diagnostic.diagnostic.severity)
6845 });
6846
6847 let marker_row_ranges = diagnostics.into_iter().map(|diagnostic| {
6848 let start_display = diagnostic
6849 .range
6850 .start
6851 .to_display_point(&snapshot.display_snapshot);
6852 let end_display = diagnostic
6853 .range
6854 .end
6855 .to_display_point(&snapshot.display_snapshot);
6856 let color = match diagnostic.diagnostic.severity {
6857 lsp::DiagnosticSeverity::ERROR => theme.status().error,
6858 lsp::DiagnosticSeverity::WARNING => theme.status().warning,
6859 lsp::DiagnosticSeverity::INFORMATION => theme.status().info,
6860 _ => theme.status().hint,
6861 };
6862 ColoredRange {
6863 start: start_display.row(),
6864 end: end_display.row(),
6865 color,
6866 }
6867 });
6868 marker_quads.extend(
6869 scrollbar_layout
6870 .marker_quads_for_ranges(marker_row_ranges, Some(2)),
6871 );
6872 }
6873
6874 Arc::from(marker_quads)
6875 })
6876 .await;
6877
6878 editor.update(cx, |editor, cx| {
6879 editor.scrollbar_marker_state.markers = scrollbar_markers;
6880 editor.scrollbar_marker_state.scrollbar_size = scrollbar_size;
6881 editor.scrollbar_marker_state.pending_refresh = None;
6882 cx.notify();
6883 })?;
6884
6885 Ok(())
6886 }));
6887 });
6888 }
6889
6890 fn paint_highlighted_range(
6891 &self,
6892 range: Range<DisplayPoint>,
6893 fill: bool,
6894 color: Hsla,
6895 corner_radius: Pixels,
6896 line_end_overshoot: Pixels,
6897 layout: &EditorLayout,
6898 window: &mut Window,
6899 ) {
6900 let start_row = layout.visible_display_row_range.start;
6901 let end_row = layout.visible_display_row_range.end;
6902 if range.start != range.end {
6903 let row_range = if range.end.column() == 0 {
6904 cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
6905 } else {
6906 cmp::max(range.start.row(), start_row)
6907 ..cmp::min(range.end.row().next_row(), end_row)
6908 };
6909
6910 let highlighted_range = HighlightedRange {
6911 color,
6912 line_height: layout.position_map.line_height,
6913 corner_radius,
6914 start_y: layout.content_origin.y
6915 + Pixels::from(
6916 (row_range.start.as_f64() - layout.position_map.scroll_position.y)
6917 * ScrollOffset::from(layout.position_map.line_height),
6918 ),
6919 lines: row_range
6920 .iter_rows()
6921 .map(|row| {
6922 let line_layout =
6923 &layout.position_map.line_layouts[row.minus(start_row) as usize];
6924 HighlightedRangeLine {
6925 start_x: if row == range.start.row() {
6926 layout.content_origin.x
6927 + Pixels::from(
6928 ScrollPixelOffset::from(
6929 line_layout.x_for_index(range.start.column() as usize),
6930 ) - layout.position_map.scroll_pixel_position.x,
6931 )
6932 } else {
6933 layout.content_origin.x
6934 - Pixels::from(layout.position_map.scroll_pixel_position.x)
6935 },
6936 end_x: if row == range.end.row() {
6937 layout.content_origin.x
6938 + Pixels::from(
6939 ScrollPixelOffset::from(
6940 line_layout.x_for_index(range.end.column() as usize),
6941 ) - layout.position_map.scroll_pixel_position.x,
6942 )
6943 } else {
6944 Pixels::from(
6945 ScrollPixelOffset::from(
6946 layout.content_origin.x
6947 + line_layout.width
6948 + line_end_overshoot,
6949 ) - layout.position_map.scroll_pixel_position.x,
6950 )
6951 },
6952 }
6953 })
6954 .collect(),
6955 };
6956
6957 highlighted_range.paint(fill, layout.position_map.text_hitbox.bounds, window);
6958 }
6959 }
6960
6961 fn paint_inline_diagnostics(
6962 &mut self,
6963 layout: &mut EditorLayout,
6964 window: &mut Window,
6965 cx: &mut App,
6966 ) {
6967 for mut inline_diagnostic in layout.inline_diagnostics.drain() {
6968 inline_diagnostic.1.paint(window, cx);
6969 }
6970 }
6971
6972 fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
6973 if let Some(mut blame_layout) = layout.inline_blame_layout.take() {
6974 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
6975 blame_layout.element.paint(window, cx);
6976 })
6977 }
6978 }
6979
6980 fn paint_inline_code_actions(
6981 &mut self,
6982 layout: &mut EditorLayout,
6983 window: &mut Window,
6984 cx: &mut App,
6985 ) {
6986 if let Some(mut inline_code_actions) = layout.inline_code_actions.take() {
6987 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
6988 inline_code_actions.paint(window, cx);
6989 })
6990 }
6991 }
6992
6993 fn paint_diff_hunk_controls(
6994 &mut self,
6995 layout: &mut EditorLayout,
6996 window: &mut Window,
6997 cx: &mut App,
6998 ) {
6999 for mut diff_hunk_control in layout.diff_hunk_controls.drain(..) {
7000 diff_hunk_control.paint(window, cx);
7001 }
7002 }
7003
7004 fn paint_minimap(&self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
7005 if let Some(mut layout) = layout.minimap.take() {
7006 let minimap_hitbox = layout.thumb_layout.hitbox.clone();
7007 let dragging_minimap = self.editor.read(cx).scroll_manager.is_dragging_minimap();
7008
7009 window.paint_layer(layout.thumb_layout.hitbox.bounds, |window| {
7010 window.with_element_namespace("minimap", |window| {
7011 layout.minimap.paint(window, cx);
7012 if let Some(thumb_bounds) = layout.thumb_layout.thumb_bounds {
7013 let minimap_thumb_color = match layout.thumb_layout.thumb_state {
7014 ScrollbarThumbState::Idle => {
7015 cx.theme().colors().minimap_thumb_background
7016 }
7017 ScrollbarThumbState::Hovered => {
7018 cx.theme().colors().minimap_thumb_hover_background
7019 }
7020 ScrollbarThumbState::Dragging => {
7021 cx.theme().colors().minimap_thumb_active_background
7022 }
7023 };
7024 let minimap_thumb_border = match layout.thumb_border_style {
7025 MinimapThumbBorder::Full => Edges::all(ScrollbarLayout::BORDER_WIDTH),
7026 MinimapThumbBorder::LeftOnly => Edges {
7027 left: ScrollbarLayout::BORDER_WIDTH,
7028 ..Default::default()
7029 },
7030 MinimapThumbBorder::LeftOpen => Edges {
7031 right: ScrollbarLayout::BORDER_WIDTH,
7032 top: ScrollbarLayout::BORDER_WIDTH,
7033 bottom: ScrollbarLayout::BORDER_WIDTH,
7034 ..Default::default()
7035 },
7036 MinimapThumbBorder::RightOpen => Edges {
7037 left: ScrollbarLayout::BORDER_WIDTH,
7038 top: ScrollbarLayout::BORDER_WIDTH,
7039 bottom: ScrollbarLayout::BORDER_WIDTH,
7040 ..Default::default()
7041 },
7042 MinimapThumbBorder::None => Default::default(),
7043 };
7044
7045 window.paint_layer(minimap_hitbox.bounds, |window| {
7046 window.paint_quad(quad(
7047 thumb_bounds,
7048 Corners::default(),
7049 minimap_thumb_color,
7050 minimap_thumb_border,
7051 cx.theme().colors().minimap_thumb_border,
7052 BorderStyle::Solid,
7053 ));
7054 });
7055 }
7056 });
7057 });
7058
7059 if dragging_minimap {
7060 window.set_window_cursor_style(CursorStyle::Arrow);
7061 } else {
7062 window.set_cursor_style(CursorStyle::Arrow, &minimap_hitbox);
7063 }
7064
7065 let minimap_axis = ScrollbarAxis::Vertical;
7066 let pixels_per_line = Pixels::from(
7067 ScrollPixelOffset::from(minimap_hitbox.size.height) / layout.max_scroll_top,
7068 )
7069 .min(layout.minimap_line_height);
7070
7071 let mut mouse_position = window.mouse_position();
7072
7073 window.on_mouse_event({
7074 let editor = self.editor.clone();
7075
7076 let minimap_hitbox = minimap_hitbox.clone();
7077
7078 move |event: &MouseMoveEvent, phase, window, cx| {
7079 if phase == DispatchPhase::Capture {
7080 return;
7081 }
7082
7083 editor.update(cx, |editor, cx| {
7084 if event.pressed_button == Some(MouseButton::Left)
7085 && editor.scroll_manager.is_dragging_minimap()
7086 {
7087 let old_position = mouse_position.along(minimap_axis);
7088 let new_position = event.position.along(minimap_axis);
7089 if (minimap_hitbox.origin.along(minimap_axis)
7090 ..minimap_hitbox.bottom_right().along(minimap_axis))
7091 .contains(&old_position)
7092 {
7093 let position =
7094 editor.scroll_position(cx).apply_along(minimap_axis, |p| {
7095 (p + ScrollPixelOffset::from(
7096 (new_position - old_position) / pixels_per_line,
7097 ))
7098 .max(0.)
7099 });
7100
7101 editor.set_scroll_position(position, window, cx);
7102 }
7103 cx.stop_propagation();
7104 } else if minimap_hitbox.is_hovered(window) {
7105 editor.scroll_manager.set_is_hovering_minimap_thumb(
7106 !event.dragging()
7107 && layout
7108 .thumb_layout
7109 .thumb_bounds
7110 .is_some_and(|bounds| bounds.contains(&event.position)),
7111 cx,
7112 );
7113
7114 // Stop hover events from propagating to the
7115 // underlying editor if the minimap hitbox is hovered
7116 if !event.dragging() {
7117 cx.stop_propagation();
7118 }
7119 } else {
7120 editor.scroll_manager.hide_minimap_thumb(cx);
7121 }
7122 mouse_position = event.position;
7123 });
7124 }
7125 });
7126
7127 if dragging_minimap {
7128 window.on_mouse_event({
7129 let editor = self.editor.clone();
7130 move |event: &MouseUpEvent, phase, window, cx| {
7131 if phase == DispatchPhase::Capture {
7132 return;
7133 }
7134
7135 editor.update(cx, |editor, cx| {
7136 if minimap_hitbox.is_hovered(window) {
7137 editor.scroll_manager.set_is_hovering_minimap_thumb(
7138 layout
7139 .thumb_layout
7140 .thumb_bounds
7141 .is_some_and(|bounds| bounds.contains(&event.position)),
7142 cx,
7143 );
7144 } else {
7145 editor.scroll_manager.hide_minimap_thumb(cx);
7146 }
7147 cx.stop_propagation();
7148 });
7149 }
7150 });
7151 } else {
7152 window.on_mouse_event({
7153 let editor = self.editor.clone();
7154
7155 move |event: &MouseDownEvent, phase, window, cx| {
7156 if phase == DispatchPhase::Capture || !minimap_hitbox.is_hovered(window) {
7157 return;
7158 }
7159
7160 let event_position = event.position;
7161
7162 let Some(thumb_bounds) = layout.thumb_layout.thumb_bounds else {
7163 return;
7164 };
7165
7166 editor.update(cx, |editor, cx| {
7167 if !thumb_bounds.contains(&event_position) {
7168 let click_position =
7169 event_position.relative_to(&minimap_hitbox.origin).y;
7170
7171 let top_position = (click_position
7172 - thumb_bounds.size.along(minimap_axis) / 2.0)
7173 .max(Pixels::ZERO);
7174
7175 let scroll_offset = (layout.minimap_scroll_top
7176 + ScrollPixelOffset::from(
7177 top_position / layout.minimap_line_height,
7178 ))
7179 .min(layout.max_scroll_top);
7180
7181 let scroll_position = editor
7182 .scroll_position(cx)
7183 .apply_along(minimap_axis, |_| scroll_offset);
7184 editor.set_scroll_position(scroll_position, window, cx);
7185 }
7186
7187 editor.scroll_manager.set_is_dragging_minimap(cx);
7188 cx.stop_propagation();
7189 });
7190 }
7191 });
7192 }
7193 }
7194 }
7195
7196 fn paint_blocks(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
7197 for mut block in layout.blocks.drain(..) {
7198 if block.overlaps_gutter {
7199 block.element.paint(window, cx);
7200 } else {
7201 let mut bounds = layout.hitbox.bounds;
7202 bounds.origin.x += layout.gutter_hitbox.bounds.size.width;
7203 window.with_content_mask(Some(ContentMask { bounds }), |window| {
7204 block.element.paint(window, cx);
7205 })
7206 }
7207 }
7208 }
7209
7210 fn paint_edit_prediction_popover(
7211 &mut self,
7212 layout: &mut EditorLayout,
7213 window: &mut Window,
7214 cx: &mut App,
7215 ) {
7216 if let Some(edit_prediction_popover) = layout.edit_prediction_popover.as_mut() {
7217 edit_prediction_popover.paint(window, cx);
7218 }
7219 }
7220
7221 fn paint_mouse_context_menu(
7222 &mut self,
7223 layout: &mut EditorLayout,
7224 window: &mut Window,
7225 cx: &mut App,
7226 ) {
7227 if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
7228 mouse_context_menu.paint(window, cx);
7229 }
7230 }
7231
7232 fn paint_scroll_wheel_listener(
7233 &mut self,
7234 layout: &EditorLayout,
7235 window: &mut Window,
7236 cx: &mut App,
7237 ) {
7238 window.on_mouse_event({
7239 let position_map = layout.position_map.clone();
7240 let editor = self.editor.clone();
7241 let hitbox = layout.hitbox.clone();
7242 let mut delta = ScrollDelta::default();
7243
7244 // Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't
7245 // accidentally turn off their scrolling.
7246 let base_scroll_sensitivity =
7247 EditorSettings::get_global(cx).scroll_sensitivity.max(0.01);
7248
7249 // Use a minimum fast_scroll_sensitivity for same reason above
7250 let fast_scroll_sensitivity = EditorSettings::get_global(cx)
7251 .fast_scroll_sensitivity
7252 .max(0.01);
7253
7254 move |event: &ScrollWheelEvent, phase, window, cx| {
7255 let scroll_sensitivity = {
7256 if event.modifiers.alt {
7257 fast_scroll_sensitivity
7258 } else {
7259 base_scroll_sensitivity
7260 }
7261 };
7262
7263 if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) {
7264 delta = delta.coalesce(event.delta);
7265 editor.update(cx, |editor, cx| {
7266 let position_map: &PositionMap = &position_map;
7267
7268 let line_height = position_map.line_height;
7269 let max_glyph_advance = position_map.em_advance;
7270 let (delta, axis) = match delta {
7271 gpui::ScrollDelta::Pixels(mut pixels) => {
7272 //Trackpad
7273 let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
7274 (pixels, axis)
7275 }
7276
7277 gpui::ScrollDelta::Lines(lines) => {
7278 //Not trackpad
7279 let pixels =
7280 point(lines.x * max_glyph_advance, lines.y * line_height);
7281 (pixels, None)
7282 }
7283 };
7284
7285 let current_scroll_position = position_map.snapshot.scroll_position();
7286 let x = (current_scroll_position.x
7287 * ScrollPixelOffset::from(max_glyph_advance)
7288 - ScrollPixelOffset::from(delta.x * scroll_sensitivity))
7289 / ScrollPixelOffset::from(max_glyph_advance);
7290 let y = (current_scroll_position.y * ScrollPixelOffset::from(line_height)
7291 - ScrollPixelOffset::from(delta.y * scroll_sensitivity))
7292 / ScrollPixelOffset::from(line_height);
7293 let mut scroll_position =
7294 point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
7295 let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll();
7296 if forbid_vertical_scroll {
7297 scroll_position.y = current_scroll_position.y;
7298 }
7299
7300 if scroll_position != current_scroll_position {
7301 editor.scroll(scroll_position, axis, window, cx);
7302 cx.stop_propagation();
7303 } else if y < 0. {
7304 // Due to clamping, we may fail to detect cases of overscroll to the top;
7305 // We want the scroll manager to get an update in such cases and detect the change of direction
7306 // on the next frame.
7307 cx.notify();
7308 }
7309 });
7310 }
7311 }
7312 });
7313 }
7314
7315 fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
7316 if layout.mode.is_minimap() {
7317 return;
7318 }
7319
7320 self.paint_scroll_wheel_listener(layout, window, cx);
7321
7322 window.on_mouse_event({
7323 let position_map = layout.position_map.clone();
7324 let editor = self.editor.clone();
7325 let line_numbers = layout.line_numbers.clone();
7326
7327 move |event: &MouseDownEvent, phase, window, cx| {
7328 if phase == DispatchPhase::Bubble {
7329 match event.button {
7330 MouseButton::Left => editor.update(cx, |editor, cx| {
7331 let pending_mouse_down = editor
7332 .pending_mouse_down
7333 .get_or_insert_with(Default::default)
7334 .clone();
7335
7336 *pending_mouse_down.borrow_mut() = Some(event.clone());
7337
7338 Self::mouse_left_down(
7339 editor,
7340 event,
7341 &position_map,
7342 line_numbers.as_ref(),
7343 window,
7344 cx,
7345 );
7346 }),
7347 MouseButton::Right => editor.update(cx, |editor, cx| {
7348 Self::mouse_right_down(editor, event, &position_map, window, cx);
7349 }),
7350 MouseButton::Middle => editor.update(cx, |editor, cx| {
7351 Self::mouse_middle_down(editor, event, &position_map, window, cx);
7352 }),
7353 _ => {}
7354 };
7355 }
7356 }
7357 });
7358
7359 window.on_mouse_event({
7360 let editor = self.editor.clone();
7361 let position_map = layout.position_map.clone();
7362
7363 move |event: &MouseUpEvent, phase, window, cx| {
7364 if phase == DispatchPhase::Bubble {
7365 editor.update(cx, |editor, cx| {
7366 Self::mouse_up(editor, event, &position_map, window, cx)
7367 });
7368 }
7369 }
7370 });
7371
7372 window.on_mouse_event({
7373 let editor = self.editor.clone();
7374 let position_map = layout.position_map.clone();
7375 let mut captured_mouse_down = None;
7376
7377 move |event: &MouseUpEvent, phase, window, cx| match phase {
7378 // Clear the pending mouse down during the capture phase,
7379 // so that it happens even if another event handler stops
7380 // propagation.
7381 DispatchPhase::Capture => editor.update(cx, |editor, _cx| {
7382 let pending_mouse_down = editor
7383 .pending_mouse_down
7384 .get_or_insert_with(Default::default)
7385 .clone();
7386
7387 let mut pending_mouse_down = pending_mouse_down.borrow_mut();
7388 if pending_mouse_down.is_some() && position_map.text_hitbox.is_hovered(window) {
7389 captured_mouse_down = pending_mouse_down.take();
7390 window.refresh();
7391 }
7392 }),
7393 // Fire click handlers during the bubble phase.
7394 DispatchPhase::Bubble => editor.update(cx, |editor, cx| {
7395 if let Some(mouse_down) = captured_mouse_down.take() {
7396 let event = ClickEvent::Mouse(MouseClickEvent {
7397 down: mouse_down,
7398 up: event.clone(),
7399 });
7400 Self::click(editor, &event, &position_map, window, cx);
7401 }
7402 }),
7403 }
7404 });
7405
7406 window.on_mouse_event({
7407 let position_map = layout.position_map.clone();
7408 let editor = self.editor.clone();
7409
7410 move |event: &MouseMoveEvent, phase, window, cx| {
7411 if phase == DispatchPhase::Bubble {
7412 editor.update(cx, |editor, cx| {
7413 if editor.hover_state.focused(window, cx) {
7414 return;
7415 }
7416 if event.pressed_button == Some(MouseButton::Left)
7417 || event.pressed_button == Some(MouseButton::Middle)
7418 {
7419 Self::mouse_dragged(editor, event, &position_map, window, cx)
7420 }
7421
7422 Self::mouse_moved(editor, event, &position_map, window, cx)
7423 });
7424 }
7425 }
7426 });
7427 }
7428
7429 fn column_pixels(&self, column: usize, window: &Window) -> Pixels {
7430 let style = &self.style;
7431 let font_size = style.text.font_size.to_pixels(window.rem_size());
7432 let layout = window.text_system().shape_line(
7433 SharedString::from(" ".repeat(column)),
7434 font_size,
7435 &[TextRun {
7436 len: column,
7437 font: style.text.font(),
7438 color: Hsla::default(),
7439 ..Default::default()
7440 }],
7441 None,
7442 );
7443
7444 layout.width
7445 }
7446
7447 fn max_line_number_width(&self, snapshot: &EditorSnapshot, window: &mut Window) -> Pixels {
7448 let digit_count = snapshot.widest_line_number().ilog10() + 1;
7449 self.column_pixels(digit_count as usize, window)
7450 }
7451
7452 fn shape_line_number(
7453 &self,
7454 text: SharedString,
7455 color: Hsla,
7456 window: &mut Window,
7457 ) -> ShapedLine {
7458 let run = TextRun {
7459 len: text.len(),
7460 font: self.style.text.font(),
7461 color,
7462 ..Default::default()
7463 };
7464 window.text_system().shape_line(
7465 text,
7466 self.style.text.font_size.to_pixels(window.rem_size()),
7467 &[run],
7468 None,
7469 )
7470 }
7471
7472 fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
7473 let unstaged = status.has_secondary_hunk();
7474 let unstaged_hollow = matches!(
7475 ProjectSettings::get_global(cx).git.hunk_style,
7476 GitHunkStyleSetting::UnstagedHollow
7477 );
7478
7479 unstaged == unstaged_hollow
7480 }
7481
7482 #[cfg(debug_assertions)]
7483 fn layout_debug_ranges(
7484 selections: &mut Vec<(PlayerColor, Vec<SelectionLayout>)>,
7485 anchor_range: Range<Anchor>,
7486 display_snapshot: &DisplaySnapshot,
7487 cx: &App,
7488 ) {
7489 let theme = cx.theme();
7490 text::debug::GlobalDebugRanges::with_locked(|debug_ranges| {
7491 if debug_ranges.ranges.is_empty() {
7492 return;
7493 }
7494 let buffer_snapshot = &display_snapshot.buffer_snapshot();
7495 for (buffer, buffer_range, excerpt_id) in
7496 buffer_snapshot.range_to_buffer_ranges(anchor_range)
7497 {
7498 let buffer_range =
7499 buffer.anchor_after(buffer_range.start)..buffer.anchor_before(buffer_range.end);
7500 selections.extend(debug_ranges.ranges.iter().flat_map(|debug_range| {
7501 let player_color = theme
7502 .players()
7503 .color_for_participant(debug_range.occurrence_index as u32 + 1);
7504 debug_range.ranges.iter().filter_map(move |range| {
7505 if range.start.buffer_id != Some(buffer.remote_id()) {
7506 return None;
7507 }
7508 let clipped_start = range.start.max(&buffer_range.start, buffer);
7509 let clipped_end = range.end.min(&buffer_range.end, buffer);
7510 let range = buffer_snapshot
7511 .anchor_range_in_excerpt(excerpt_id, *clipped_start..*clipped_end)?;
7512 let start = range.start.to_display_point(display_snapshot);
7513 let end = range.end.to_display_point(display_snapshot);
7514 let selection_layout = SelectionLayout {
7515 head: start,
7516 range: start..end,
7517 cursor_shape: CursorShape::Bar,
7518 is_newest: false,
7519 is_local: false,
7520 active_rows: start.row()..end.row(),
7521 user_name: Some(SharedString::new(debug_range.value.clone())),
7522 };
7523 Some((player_color, vec![selection_layout]))
7524 })
7525 }));
7526 }
7527 });
7528 }
7529}
7530
7531fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
7532 file_status.map_or(Color::Default, |status| {
7533 if status.is_conflicted() {
7534 Color::Conflict
7535 } else if status.is_modified() {
7536 Color::Modified
7537 } else if status.is_deleted() {
7538 Color::Disabled
7539 } else if status.is_created() {
7540 Color::Created
7541 } else {
7542 Color::Default
7543 }
7544 })
7545}
7546
7547fn header_jump_data(
7548 snapshot: &EditorSnapshot,
7549 block_row_start: DisplayRow,
7550 height: u32,
7551 for_excerpt: &ExcerptInfo,
7552) -> JumpData {
7553 let range = &for_excerpt.range;
7554 let buffer = &for_excerpt.buffer;
7555 let jump_anchor = range.primary.start;
7556
7557 let excerpt_start = range.context.start;
7558 let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
7559 let rows_from_excerpt_start = if jump_anchor == excerpt_start {
7560 0
7561 } else {
7562 let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
7563 jump_position.row.saturating_sub(excerpt_start_point.row)
7564 };
7565
7566 let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start)
7567 .saturating_sub(
7568 snapshot
7569 .scroll_anchor
7570 .scroll_position(&snapshot.display_snapshot)
7571 .y as u32,
7572 );
7573
7574 JumpData::MultiBufferPoint {
7575 excerpt_id: for_excerpt.id,
7576 anchor: jump_anchor,
7577 position: jump_position,
7578 line_offset_from_top,
7579 }
7580}
7581
7582pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
7583
7584impl AcceptEditPredictionBinding {
7585 pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
7586 if let Some(binding) = self.0.as_ref() {
7587 match &binding.keystrokes() {
7588 [keystroke, ..] => Some(keystroke),
7589 _ => None,
7590 }
7591 } else {
7592 None
7593 }
7594 }
7595}
7596
7597fn prepaint_gutter_button(
7598 button: IconButton,
7599 row: DisplayRow,
7600 line_height: Pixels,
7601 gutter_dimensions: &GutterDimensions,
7602 scroll_position: gpui::Point<ScrollOffset>,
7603 gutter_hitbox: &Hitbox,
7604 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
7605 window: &mut Window,
7606 cx: &mut App,
7607) -> AnyElement {
7608 let mut button = button.into_any_element();
7609
7610 let available_space = size(
7611 AvailableSpace::MinContent,
7612 AvailableSpace::Definite(line_height),
7613 );
7614 let indicator_size = button.layout_as_root(available_space, window, cx);
7615
7616 let blame_width = gutter_dimensions.git_blame_entries_width;
7617 let gutter_width = display_hunks
7618 .binary_search_by(|(hunk, _)| match hunk {
7619 DisplayDiffHunk::Folded { display_row } => display_row.cmp(&row),
7620 DisplayDiffHunk::Unfolded {
7621 display_row_range, ..
7622 } => {
7623 if display_row_range.end <= row {
7624 Ordering::Less
7625 } else if display_row_range.start > row {
7626 Ordering::Greater
7627 } else {
7628 Ordering::Equal
7629 }
7630 }
7631 })
7632 .ok()
7633 .and_then(|ix| Some(display_hunks[ix].1.as_ref()?.size.width));
7634 let left_offset = blame_width.max(gutter_width).unwrap_or_default();
7635
7636 let mut x = left_offset;
7637 let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
7638 - indicator_size.width
7639 - left_offset;
7640 x += available_width / 2.;
7641
7642 let mut y =
7643 Pixels::from((row.as_f64() - scroll_position.y) * ScrollPixelOffset::from(line_height));
7644 y += (line_height - indicator_size.height) / 2.;
7645
7646 button.prepaint_as_root(
7647 gutter_hitbox.origin + point(x, y),
7648 available_space,
7649 window,
7650 cx,
7651 );
7652 button
7653}
7654
7655fn render_inline_blame_entry(
7656 blame_entry: BlameEntry,
7657 style: &EditorStyle,
7658 cx: &mut App,
7659) -> Option<AnyElement> {
7660 let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
7661 renderer.render_inline_blame_entry(&style.text, blame_entry, cx)
7662}
7663
7664fn render_blame_entry_popover(
7665 blame_entry: BlameEntry,
7666 scroll_handle: ScrollHandle,
7667 commit_message: Option<ParsedCommitMessage>,
7668 markdown: Entity<Markdown>,
7669 workspace: WeakEntity<Workspace>,
7670 blame: &Entity<GitBlame>,
7671 buffer: BufferId,
7672 window: &mut Window,
7673 cx: &mut App,
7674) -> Option<AnyElement> {
7675 let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
7676 let blame = blame.read(cx);
7677 let repository = blame.repository(cx, buffer)?;
7678 renderer.render_blame_entry_popover(
7679 blame_entry,
7680 scroll_handle,
7681 commit_message,
7682 markdown,
7683 repository,
7684 workspace,
7685 window,
7686 cx,
7687 )
7688}
7689
7690fn render_blame_entry(
7691 ix: usize,
7692 blame: &Entity<GitBlame>,
7693 blame_entry: BlameEntry,
7694 style: &EditorStyle,
7695 last_used_color: &mut Option<(Hsla, Oid)>,
7696 editor: Entity<Editor>,
7697 workspace: Entity<Workspace>,
7698 buffer: BufferId,
7699 renderer: &dyn BlameRenderer,
7700 window: &mut Window,
7701 cx: &mut App,
7702) -> Option<AnyElement> {
7703 let index: u32 = blame_entry.sha.into();
7704 let mut sha_color = cx.theme().players().color_for_participant(index).cursor;
7705
7706 // If the last color we used is the same as the one we get for this line, but
7707 // the commit SHAs are different, then we try again to get a different color.
7708 if let Some((color, sha)) = *last_used_color
7709 && sha != blame_entry.sha
7710 && color == sha_color
7711 {
7712 sha_color = cx.theme().players().color_for_participant(index + 1).cursor;
7713 }
7714 last_used_color.replace((sha_color, blame_entry.sha));
7715
7716 let blame = blame.read(cx);
7717 let details = blame.details_for_entry(buffer, &blame_entry);
7718 let repository = blame.repository(cx, buffer)?;
7719 renderer.render_blame_entry(
7720 &style.text,
7721 blame_entry,
7722 details,
7723 repository,
7724 workspace.downgrade(),
7725 editor,
7726 ix,
7727 sha_color,
7728 window,
7729 cx,
7730 )
7731}
7732
7733#[derive(Debug)]
7734pub(crate) struct LineWithInvisibles {
7735 fragments: SmallVec<[LineFragment; 1]>,
7736 invisibles: Vec<Invisible>,
7737 len: usize,
7738 pub(crate) width: Pixels,
7739 font_size: Pixels,
7740}
7741
7742enum LineFragment {
7743 Text(ShapedLine),
7744 Element {
7745 id: ChunkRendererId,
7746 element: Option<AnyElement>,
7747 size: Size<Pixels>,
7748 len: usize,
7749 },
7750}
7751
7752impl fmt::Debug for LineFragment {
7753 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
7754 match self {
7755 LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
7756 LineFragment::Element { size, len, .. } => f
7757 .debug_struct("Element")
7758 .field("size", size)
7759 .field("len", len)
7760 .finish(),
7761 }
7762 }
7763}
7764
7765impl LineWithInvisibles {
7766 fn from_chunks<'a>(
7767 chunks: impl Iterator<Item = HighlightedChunk<'a>>,
7768 editor_style: &EditorStyle,
7769 max_line_len: usize,
7770 max_line_count: usize,
7771 editor_mode: &EditorMode,
7772 text_width: Pixels,
7773 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
7774 bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
7775 window: &mut Window,
7776 cx: &mut App,
7777 ) -> Vec<Self> {
7778 let text_style = &editor_style.text;
7779 let mut layouts = Vec::with_capacity(max_line_count);
7780 let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
7781 let mut line = String::new();
7782 let mut invisibles = Vec::new();
7783 let mut width = Pixels::ZERO;
7784 let mut len = 0;
7785 let mut styles = Vec::new();
7786 let mut non_whitespace_added = false;
7787 let mut row = 0;
7788 let mut line_exceeded_max_len = false;
7789 let font_size = text_style.font_size.to_pixels(window.rem_size());
7790 let min_contrast = EditorSettings::get_global(cx).minimum_contrast_for_highlights;
7791
7792 let ellipsis = SharedString::from("β―");
7793
7794 for highlighted_chunk in chunks.chain([HighlightedChunk {
7795 text: "\n",
7796 style: None,
7797 is_tab: false,
7798 is_inlay: false,
7799 replacement: None,
7800 }]) {
7801 if let Some(replacement) = highlighted_chunk.replacement {
7802 if !line.is_empty() {
7803 let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
7804 let text_runs: &[TextRun] = if segments.is_empty() {
7805 &styles
7806 } else {
7807 &Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
7808 };
7809 let shaped_line = window.text_system().shape_line(
7810 line.clone().into(),
7811 font_size,
7812 text_runs,
7813 None,
7814 );
7815 width += shaped_line.width;
7816 len += shaped_line.len;
7817 fragments.push(LineFragment::Text(shaped_line));
7818 line.clear();
7819 styles.clear();
7820 }
7821
7822 match replacement {
7823 ChunkReplacement::Renderer(renderer) => {
7824 let available_width = if renderer.constrain_width {
7825 let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
7826 ellipsis.clone()
7827 } else {
7828 SharedString::from(Arc::from(highlighted_chunk.text))
7829 };
7830 let shaped_line = window.text_system().shape_line(
7831 chunk,
7832 font_size,
7833 &[text_style.to_run(highlighted_chunk.text.len())],
7834 None,
7835 );
7836 AvailableSpace::Definite(shaped_line.width)
7837 } else {
7838 AvailableSpace::MinContent
7839 };
7840
7841 let mut element = (renderer.render)(&mut ChunkRendererContext {
7842 context: cx,
7843 window,
7844 max_width: text_width,
7845 });
7846 let line_height = text_style.line_height_in_pixels(window.rem_size());
7847 let size = element.layout_as_root(
7848 size(available_width, AvailableSpace::Definite(line_height)),
7849 window,
7850 cx,
7851 );
7852
7853 width += size.width;
7854 len += highlighted_chunk.text.len();
7855 fragments.push(LineFragment::Element {
7856 id: renderer.id,
7857 element: Some(element),
7858 size,
7859 len: highlighted_chunk.text.len(),
7860 });
7861 }
7862 ChunkReplacement::Str(x) => {
7863 let text_style = if let Some(style) = highlighted_chunk.style {
7864 Cow::Owned(text_style.clone().highlight(style))
7865 } else {
7866 Cow::Borrowed(text_style)
7867 };
7868
7869 let run = TextRun {
7870 len: x.len(),
7871 font: text_style.font(),
7872 color: text_style.color,
7873 background_color: text_style.background_color,
7874 underline: text_style.underline,
7875 strikethrough: text_style.strikethrough,
7876 };
7877 let line_layout = window
7878 .text_system()
7879 .shape_line(x, font_size, &[run], None)
7880 .with_len(highlighted_chunk.text.len());
7881
7882 width += line_layout.width;
7883 len += highlighted_chunk.text.len();
7884 fragments.push(LineFragment::Text(line_layout))
7885 }
7886 }
7887 } else {
7888 for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
7889 if ix > 0 {
7890 let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
7891 let text_runs = if segments.is_empty() {
7892 &styles
7893 } else {
7894 &Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
7895 };
7896 let shaped_line = window.text_system().shape_line(
7897 line.clone().into(),
7898 font_size,
7899 text_runs,
7900 None,
7901 );
7902 width += shaped_line.width;
7903 len += shaped_line.len;
7904 fragments.push(LineFragment::Text(shaped_line));
7905 layouts.push(Self {
7906 width: mem::take(&mut width),
7907 len: mem::take(&mut len),
7908 fragments: mem::take(&mut fragments),
7909 invisibles: std::mem::take(&mut invisibles),
7910 font_size,
7911 });
7912
7913 line.clear();
7914 styles.clear();
7915 row += 1;
7916 line_exceeded_max_len = false;
7917 non_whitespace_added = false;
7918 if row == max_line_count {
7919 return layouts;
7920 }
7921 }
7922
7923 if !line_chunk.is_empty() && !line_exceeded_max_len {
7924 let text_style = if let Some(style) = highlighted_chunk.style {
7925 Cow::Owned(text_style.clone().highlight(style))
7926 } else {
7927 Cow::Borrowed(text_style)
7928 };
7929
7930 if line.len() + line_chunk.len() > max_line_len {
7931 let mut chunk_len = max_line_len - line.len();
7932 while !line_chunk.is_char_boundary(chunk_len) {
7933 chunk_len -= 1;
7934 }
7935 line_chunk = &line_chunk[..chunk_len];
7936 line_exceeded_max_len = true;
7937 }
7938
7939 styles.push(TextRun {
7940 len: line_chunk.len(),
7941 font: text_style.font(),
7942 color: text_style.color,
7943 background_color: text_style.background_color,
7944 underline: text_style.underline,
7945 strikethrough: text_style.strikethrough,
7946 });
7947
7948 if editor_mode.is_full() && !highlighted_chunk.is_inlay {
7949 // Line wrap pads its contents with fake whitespaces,
7950 // avoid printing them
7951 let is_soft_wrapped = is_row_soft_wrapped(row);
7952 if highlighted_chunk.is_tab {
7953 if non_whitespace_added || !is_soft_wrapped {
7954 invisibles.push(Invisible::Tab {
7955 line_start_offset: line.len(),
7956 line_end_offset: line.len() + line_chunk.len(),
7957 });
7958 }
7959 } else {
7960 invisibles.extend(line_chunk.char_indices().filter_map(
7961 |(index, c)| {
7962 let is_whitespace = c.is_whitespace();
7963 non_whitespace_added |= !is_whitespace;
7964 if is_whitespace
7965 && (non_whitespace_added || !is_soft_wrapped)
7966 {
7967 Some(Invisible::Whitespace {
7968 line_offset: line.len() + index,
7969 })
7970 } else {
7971 None
7972 }
7973 },
7974 ))
7975 }
7976 }
7977
7978 line.push_str(line_chunk);
7979 }
7980 }
7981 }
7982 }
7983
7984 layouts
7985 }
7986
7987 /// Takes text runs and non-overlapping left-to-right background ranges with color.
7988 /// Returns new text runs with adjusted contrast as per background ranges.
7989 fn split_runs_by_bg_segments(
7990 text_runs: &[TextRun],
7991 bg_segments: &[(Range<DisplayPoint>, Hsla)],
7992 min_contrast: f32,
7993 start_col_offset: usize,
7994 ) -> Vec<TextRun> {
7995 let mut output_runs: Vec<TextRun> = Vec::with_capacity(text_runs.len());
7996 let mut line_col = start_col_offset;
7997 let mut segment_ix = 0usize;
7998
7999 for text_run in text_runs.iter() {
8000 let run_start_col = line_col;
8001 let run_end_col = run_start_col + text_run.len;
8002 while segment_ix < bg_segments.len()
8003 && (bg_segments[segment_ix].0.end.column() as usize) <= run_start_col
8004 {
8005 segment_ix += 1;
8006 }
8007 let mut cursor_col = run_start_col;
8008 let mut local_segment_ix = segment_ix;
8009 while local_segment_ix < bg_segments.len() {
8010 let (range, segment_color) = &bg_segments[local_segment_ix];
8011 let segment_start_col = range.start.column() as usize;
8012 let segment_end_col = range.end.column() as usize;
8013 if segment_start_col >= run_end_col {
8014 break;
8015 }
8016 if segment_start_col > cursor_col {
8017 let span_len = segment_start_col - cursor_col;
8018 output_runs.push(TextRun {
8019 len: span_len,
8020 font: text_run.font.clone(),
8021 color: text_run.color,
8022 background_color: text_run.background_color,
8023 underline: text_run.underline,
8024 strikethrough: text_run.strikethrough,
8025 });
8026 cursor_col = segment_start_col;
8027 }
8028 let segment_slice_end_col = segment_end_col.min(run_end_col);
8029 if segment_slice_end_col > cursor_col {
8030 let new_text_color =
8031 ensure_minimum_contrast(text_run.color, *segment_color, min_contrast);
8032 output_runs.push(TextRun {
8033 len: segment_slice_end_col - cursor_col,
8034 font: text_run.font.clone(),
8035 color: new_text_color,
8036 background_color: text_run.background_color,
8037 underline: text_run.underline,
8038 strikethrough: text_run.strikethrough,
8039 });
8040 cursor_col = segment_slice_end_col;
8041 }
8042 if segment_end_col >= run_end_col {
8043 break;
8044 }
8045 local_segment_ix += 1;
8046 }
8047 if cursor_col < run_end_col {
8048 output_runs.push(TextRun {
8049 len: run_end_col - cursor_col,
8050 font: text_run.font.clone(),
8051 color: text_run.color,
8052 background_color: text_run.background_color,
8053 underline: text_run.underline,
8054 strikethrough: text_run.strikethrough,
8055 });
8056 }
8057 line_col = run_end_col;
8058 segment_ix = local_segment_ix;
8059 }
8060 output_runs
8061 }
8062
8063 fn prepaint(
8064 &mut self,
8065 line_height: Pixels,
8066 scroll_position: gpui::Point<ScrollOffset>,
8067 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
8068 row: DisplayRow,
8069 content_origin: gpui::Point<Pixels>,
8070 line_elements: &mut SmallVec<[AnyElement; 1]>,
8071 window: &mut Window,
8072 cx: &mut App,
8073 ) {
8074 let line_y = f32::from(line_height) * Pixels::from(row.as_f64() - scroll_position.y);
8075 let mut fragment_origin =
8076 content_origin + gpui::point(Pixels::from(-scroll_pixel_position.x), line_y);
8077 for fragment in &mut self.fragments {
8078 match fragment {
8079 LineFragment::Text(line) => {
8080 fragment_origin.x += line.width;
8081 }
8082 LineFragment::Element { element, size, .. } => {
8083 let mut element = element
8084 .take()
8085 .expect("you can't prepaint LineWithInvisibles twice");
8086
8087 // Center the element vertically within the line.
8088 let mut element_origin = fragment_origin;
8089 element_origin.y += (line_height - size.height) / 2.;
8090 element.prepaint_at(element_origin, window, cx);
8091 line_elements.push(element);
8092
8093 fragment_origin.x += size.width;
8094 }
8095 }
8096 }
8097 }
8098
8099 fn draw(
8100 &self,
8101 layout: &EditorLayout,
8102 row: DisplayRow,
8103 content_origin: gpui::Point<Pixels>,
8104 whitespace_setting: ShowWhitespaceSetting,
8105 selection_ranges: &[Range<DisplayPoint>],
8106 window: &mut Window,
8107 cx: &mut App,
8108 ) {
8109 let line_height = layout.position_map.line_height;
8110 let line_y = line_height * (row.as_f64() - layout.position_map.scroll_position.y) as f32;
8111
8112 let mut fragment_origin = content_origin
8113 + gpui::point(
8114 Pixels::from(-layout.position_map.scroll_pixel_position.x),
8115 line_y,
8116 );
8117
8118 for fragment in &self.fragments {
8119 match fragment {
8120 LineFragment::Text(line) => {
8121 line.paint(fragment_origin, line_height, window, cx)
8122 .log_err();
8123 fragment_origin.x += line.width;
8124 }
8125 LineFragment::Element { size, .. } => {
8126 fragment_origin.x += size.width;
8127 }
8128 }
8129 }
8130
8131 self.draw_invisibles(
8132 selection_ranges,
8133 layout,
8134 content_origin,
8135 line_y,
8136 row,
8137 line_height,
8138 whitespace_setting,
8139 window,
8140 cx,
8141 );
8142 }
8143
8144 fn draw_background(
8145 &self,
8146 layout: &EditorLayout,
8147 row: DisplayRow,
8148 content_origin: gpui::Point<Pixels>,
8149 window: &mut Window,
8150 cx: &mut App,
8151 ) {
8152 let line_height = layout.position_map.line_height;
8153 let line_y = line_height * (row.as_f64() - layout.position_map.scroll_position.y) as f32;
8154
8155 let mut fragment_origin = content_origin
8156 + gpui::point(
8157 Pixels::from(-layout.position_map.scroll_pixel_position.x),
8158 line_y,
8159 );
8160
8161 for fragment in &self.fragments {
8162 match fragment {
8163 LineFragment::Text(line) => {
8164 line.paint_background(fragment_origin, line_height, window, cx)
8165 .log_err();
8166 fragment_origin.x += line.width;
8167 }
8168 LineFragment::Element { size, .. } => {
8169 fragment_origin.x += size.width;
8170 }
8171 }
8172 }
8173 }
8174
8175 fn draw_invisibles(
8176 &self,
8177 selection_ranges: &[Range<DisplayPoint>],
8178 layout: &EditorLayout,
8179 content_origin: gpui::Point<Pixels>,
8180 line_y: Pixels,
8181 row: DisplayRow,
8182 line_height: Pixels,
8183 whitespace_setting: ShowWhitespaceSetting,
8184 window: &mut Window,
8185 cx: &mut App,
8186 ) {
8187 let extract_whitespace_info = |invisible: &Invisible| {
8188 let (token_offset, token_end_offset, invisible_symbol) = match invisible {
8189 Invisible::Tab {
8190 line_start_offset,
8191 line_end_offset,
8192 } => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
8193 Invisible::Whitespace { line_offset } => {
8194 (*line_offset, line_offset + 1, &layout.space_invisible)
8195 }
8196 };
8197
8198 let x_offset: ScrollPixelOffset = self.x_for_index(token_offset).into();
8199 let invisible_offset: ScrollPixelOffset =
8200 ((layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0)
8201 .into();
8202 let origin = content_origin
8203 + gpui::point(
8204 Pixels::from(
8205 x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x,
8206 ),
8207 line_y,
8208 );
8209
8210 (
8211 [token_offset, token_end_offset],
8212 Box::new(move |window: &mut Window, cx: &mut App| {
8213 invisible_symbol
8214 .paint(origin, line_height, window, cx)
8215 .log_err();
8216 }),
8217 )
8218 };
8219
8220 let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
8221 match whitespace_setting {
8222 ShowWhitespaceSetting::None => (),
8223 ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(window, cx)),
8224 ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
8225 let invisible_point = DisplayPoint::new(row, start as u32);
8226 if !selection_ranges
8227 .iter()
8228 .any(|region| region.start <= invisible_point && invisible_point < region.end)
8229 {
8230 return;
8231 }
8232
8233 paint(window, cx);
8234 }),
8235
8236 ShowWhitespaceSetting::Trailing => {
8237 let mut previous_start = self.len;
8238 for ([start, end], paint) in invisible_iter.rev() {
8239 if previous_start != end {
8240 break;
8241 }
8242 previous_start = start;
8243 paint(window, cx);
8244 }
8245 }
8246
8247 // For a whitespace to be on a boundary, any of the following conditions need to be met:
8248 // - It is a tab
8249 // - It is adjacent to an edge (start or end)
8250 // - It is adjacent to a whitespace (left or right)
8251 ShowWhitespaceSetting::Boundary => {
8252 // 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
8253 // the above cases.
8254 // Note: We zip in the original `invisibles` to check for tab equality
8255 let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut Window, &mut App)>)> = None;
8256 for (([start, end], paint), invisible) in
8257 invisible_iter.zip_eq(self.invisibles.iter())
8258 {
8259 let should_render = match (&last_seen, invisible) {
8260 (_, Invisible::Tab { .. }) => true,
8261 (Some((_, last_end, _)), _) => *last_end == start,
8262 _ => false,
8263 };
8264
8265 if should_render || start == 0 || end == self.len {
8266 paint(window, cx);
8267
8268 // Since we are scanning from the left, we will skip over the first available whitespace that is part
8269 // of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
8270 if let Some((should_render_last, last_end, paint_last)) = last_seen {
8271 // Note that we need to make sure that the last one is actually adjacent
8272 if !should_render_last && last_end == start {
8273 paint_last(window, cx);
8274 }
8275 }
8276 }
8277
8278 // Manually render anything within a selection
8279 let invisible_point = DisplayPoint::new(row, start as u32);
8280 if selection_ranges.iter().any(|region| {
8281 region.start <= invisible_point && invisible_point < region.end
8282 }) {
8283 paint(window, cx);
8284 }
8285
8286 last_seen = Some((should_render, end, paint));
8287 }
8288 }
8289 }
8290 }
8291
8292 pub fn x_for_index(&self, index: usize) -> Pixels {
8293 let mut fragment_start_x = Pixels::ZERO;
8294 let mut fragment_start_index = 0;
8295
8296 for fragment in &self.fragments {
8297 match fragment {
8298 LineFragment::Text(shaped_line) => {
8299 let fragment_end_index = fragment_start_index + shaped_line.len;
8300 if index < fragment_end_index {
8301 return fragment_start_x
8302 + shaped_line.x_for_index(index - fragment_start_index);
8303 }
8304 fragment_start_x += shaped_line.width;
8305 fragment_start_index = fragment_end_index;
8306 }
8307 LineFragment::Element { len, size, .. } => {
8308 let fragment_end_index = fragment_start_index + len;
8309 if index < fragment_end_index {
8310 return fragment_start_x;
8311 }
8312 fragment_start_x += size.width;
8313 fragment_start_index = fragment_end_index;
8314 }
8315 }
8316 }
8317
8318 fragment_start_x
8319 }
8320
8321 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
8322 let mut fragment_start_x = Pixels::ZERO;
8323 let mut fragment_start_index = 0;
8324
8325 for fragment in &self.fragments {
8326 match fragment {
8327 LineFragment::Text(shaped_line) => {
8328 let fragment_end_x = fragment_start_x + shaped_line.width;
8329 if x < fragment_end_x {
8330 return Some(
8331 fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
8332 );
8333 }
8334 fragment_start_x = fragment_end_x;
8335 fragment_start_index += shaped_line.len;
8336 }
8337 LineFragment::Element { len, size, .. } => {
8338 let fragment_end_x = fragment_start_x + size.width;
8339 if x < fragment_end_x {
8340 return Some(fragment_start_index);
8341 }
8342 fragment_start_index += len;
8343 fragment_start_x = fragment_end_x;
8344 }
8345 }
8346 }
8347
8348 None
8349 }
8350
8351 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
8352 let mut fragment_start_index = 0;
8353
8354 for fragment in &self.fragments {
8355 match fragment {
8356 LineFragment::Text(shaped_line) => {
8357 let fragment_end_index = fragment_start_index + shaped_line.len;
8358 if index < fragment_end_index {
8359 return shaped_line.font_id_for_index(index - fragment_start_index);
8360 }
8361 fragment_start_index = fragment_end_index;
8362 }
8363 LineFragment::Element { len, .. } => {
8364 let fragment_end_index = fragment_start_index + len;
8365 if index < fragment_end_index {
8366 return None;
8367 }
8368 fragment_start_index = fragment_end_index;
8369 }
8370 }
8371 }
8372
8373 None
8374 }
8375}
8376
8377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8378enum Invisible {
8379 /// A tab character
8380 ///
8381 /// A tab character is internally represented by spaces (configured by the user's tab width)
8382 /// aligned to the nearest column, so it's necessary to store the start and end offset for
8383 /// adjacency checks.
8384 Tab {
8385 line_start_offset: usize,
8386 line_end_offset: usize,
8387 },
8388 Whitespace {
8389 line_offset: usize,
8390 },
8391}
8392
8393impl EditorElement {
8394 /// Returns the rem size to use when rendering the [`EditorElement`].
8395 ///
8396 /// This allows UI elements to scale based on the `buffer_font_size`.
8397 fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
8398 match self.editor.read(cx).mode {
8399 EditorMode::Full {
8400 scale_ui_elements_with_buffer_font_size: true,
8401 ..
8402 }
8403 | EditorMode::Minimap { .. } => {
8404 let buffer_font_size = self.style.text.font_size;
8405 match buffer_font_size {
8406 AbsoluteLength::Pixels(pixels) => {
8407 let rem_size_scale = {
8408 // Our default UI font size is 14px on a 16px base scale.
8409 // This means the default UI font size is 0.875rems.
8410 let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
8411
8412 // We then determine the delta between a single rem and the default font
8413 // size scale.
8414 let default_font_size_delta = 1. - default_font_size_scale;
8415
8416 // Finally, we add this delta to 1rem to get the scale factor that
8417 // should be used to scale up the UI.
8418 1. + default_font_size_delta
8419 };
8420
8421 Some(pixels * rem_size_scale)
8422 }
8423 AbsoluteLength::Rems(rems) => {
8424 Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into()))
8425 }
8426 }
8427 }
8428 // We currently use single-line and auto-height editors in UI contexts,
8429 // so we don't want to scale everything with the buffer font size, as it
8430 // ends up looking off.
8431 _ => None,
8432 }
8433 }
8434
8435 fn editor_with_selections(&self, cx: &App) -> Option<Entity<Editor>> {
8436 if let EditorMode::Minimap { parent } = self.editor.read(cx).mode() {
8437 parent.upgrade()
8438 } else {
8439 Some(self.editor.clone())
8440 }
8441 }
8442}
8443
8444impl Element for EditorElement {
8445 type RequestLayoutState = ();
8446 type PrepaintState = EditorLayout;
8447
8448 fn id(&self) -> Option<ElementId> {
8449 None
8450 }
8451
8452 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
8453 None
8454 }
8455
8456 fn request_layout(
8457 &mut self,
8458 _: Option<&GlobalElementId>,
8459 _inspector_id: Option<&gpui::InspectorElementId>,
8460 window: &mut Window,
8461 cx: &mut App,
8462 ) -> (gpui::LayoutId, ()) {
8463 let rem_size = self.rem_size(cx);
8464 window.with_rem_size(rem_size, |window| {
8465 self.editor.update(cx, |editor, cx| {
8466 editor.set_style(self.style.clone(), window, cx);
8467
8468 let layout_id = match editor.mode {
8469 EditorMode::SingleLine => {
8470 let rem_size = window.rem_size();
8471 let height = self.style.text.line_height_in_pixels(rem_size);
8472 let mut style = Style::default();
8473 style.size.height = height.into();
8474 style.size.width = relative(1.).into();
8475 window.request_layout(style, None, cx)
8476 }
8477 EditorMode::AutoHeight {
8478 min_lines,
8479 max_lines,
8480 } => {
8481 let editor_handle = cx.entity();
8482 let max_line_number_width =
8483 self.max_line_number_width(&editor.snapshot(window, cx), window);
8484 window.request_measured_layout(
8485 Style::default(),
8486 move |known_dimensions, available_space, window, cx| {
8487 editor_handle
8488 .update(cx, |editor, cx| {
8489 compute_auto_height_layout(
8490 editor,
8491 min_lines,
8492 max_lines,
8493 max_line_number_width,
8494 known_dimensions,
8495 available_space.width,
8496 window,
8497 cx,
8498 )
8499 })
8500 .unwrap_or_default()
8501 },
8502 )
8503 }
8504 EditorMode::Minimap { .. } => {
8505 let mut style = Style::default();
8506 style.size.width = relative(1.).into();
8507 style.size.height = relative(1.).into();
8508 window.request_layout(style, None, cx)
8509 }
8510 EditorMode::Full {
8511 sizing_behavior, ..
8512 } => {
8513 let mut style = Style::default();
8514 style.size.width = relative(1.).into();
8515 if sizing_behavior == SizingBehavior::SizeByContent {
8516 let snapshot = editor.snapshot(window, cx);
8517 let line_height =
8518 self.style.text.line_height_in_pixels(window.rem_size());
8519 let scroll_height =
8520 (snapshot.max_point().row().next_row().0 as f32) * line_height;
8521 style.size.height = scroll_height.into();
8522 } else {
8523 style.size.height = relative(1.).into();
8524 }
8525 window.request_layout(style, None, cx)
8526 }
8527 };
8528
8529 (layout_id, ())
8530 })
8531 })
8532 }
8533
8534 fn prepaint(
8535 &mut self,
8536 _: Option<&GlobalElementId>,
8537 _inspector_id: Option<&gpui::InspectorElementId>,
8538 bounds: Bounds<Pixels>,
8539 _: &mut Self::RequestLayoutState,
8540 window: &mut Window,
8541 cx: &mut App,
8542 ) -> Self::PrepaintState {
8543 let text_style = TextStyleRefinement {
8544 font_size: Some(self.style.text.font_size),
8545 line_height: Some(self.style.text.line_height),
8546 ..Default::default()
8547 };
8548
8549 let is_minimap = self.editor.read(cx).mode.is_minimap();
8550
8551 if !is_minimap {
8552 let focus_handle = self.editor.focus_handle(cx);
8553 window.set_view_id(self.editor.entity_id());
8554 window.set_focus_handle(&focus_handle, cx);
8555 }
8556
8557 let rem_size = self.rem_size(cx);
8558 window.with_rem_size(rem_size, |window| {
8559 window.with_text_style(Some(text_style), |window| {
8560 window.with_content_mask(Some(ContentMask { bounds }), |window| {
8561 let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
8562 (editor.snapshot(window, cx), editor.read_only(cx))
8563 });
8564 let style = &self.style;
8565
8566 let rem_size = window.rem_size();
8567 let font_id = window.text_system().resolve_font(&style.text.font());
8568 let font_size = style.text.font_size.to_pixels(rem_size);
8569 let line_height = style.text.line_height_in_pixels(rem_size);
8570 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
8571 let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
8572 let glyph_grid_cell = size(em_advance, line_height);
8573
8574 let gutter_dimensions = snapshot
8575 .gutter_dimensions(
8576 font_id,
8577 font_size,
8578 self.max_line_number_width(&snapshot, window),
8579 cx,
8580 )
8581 .or_else(|| {
8582 self.editor.read(cx).offset_content.then(|| {
8583 GutterDimensions::default_with_margin(font_id, font_size, cx)
8584 })
8585 })
8586 .unwrap_or_default();
8587 let text_width = bounds.size.width - gutter_dimensions.width;
8588
8589 let settings = EditorSettings::get_global(cx);
8590 let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never;
8591 let vertical_scrollbar_width = (scrollbars_shown
8592 && settings.scrollbar.axes.vertical
8593 && self.editor.read(cx).show_scrollbars.vertical)
8594 .then_some(style.scrollbar_width)
8595 .unwrap_or_default();
8596 let minimap_width = self
8597 .get_minimap_width(
8598 &settings.minimap,
8599 scrollbars_shown,
8600 text_width,
8601 em_width,
8602 font_size,
8603 rem_size,
8604 cx,
8605 )
8606 .unwrap_or_default();
8607
8608 let right_margin = minimap_width + vertical_scrollbar_width;
8609
8610 let editor_width =
8611 text_width - gutter_dimensions.margin - 2 * em_width - right_margin;
8612 let editor_margins = EditorMargins {
8613 gutter: gutter_dimensions,
8614 right: right_margin,
8615 };
8616
8617 snapshot = self.editor.update(cx, |editor, cx| {
8618 editor.last_bounds = Some(bounds);
8619 editor.gutter_dimensions = gutter_dimensions;
8620 editor.set_visible_line_count(
8621 (bounds.size.height / line_height) as f64,
8622 window,
8623 cx,
8624 );
8625 editor.set_visible_column_count(f64::from(editor_width / em_advance));
8626
8627 if matches!(
8628 editor.mode,
8629 EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
8630 ) {
8631 snapshot
8632 } else {
8633 let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
8634 let wrap_width = match editor.soft_wrap_mode(cx) {
8635 SoftWrap::GitDiff => None,
8636 SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
8637 SoftWrap::EditorWidth => Some(editor_width),
8638 SoftWrap::Column(column) => Some(wrap_width_for(column)),
8639 SoftWrap::Bounded(column) => {
8640 Some(editor_width.min(wrap_width_for(column)))
8641 }
8642 };
8643
8644 if editor.set_wrap_width(wrap_width, cx) {
8645 editor.snapshot(window, cx)
8646 } else {
8647 snapshot
8648 }
8649 }
8650 });
8651
8652 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
8653 let gutter_hitbox = window.insert_hitbox(
8654 gutter_bounds(bounds, gutter_dimensions),
8655 HitboxBehavior::Normal,
8656 );
8657 let text_hitbox = window.insert_hitbox(
8658 Bounds {
8659 origin: gutter_hitbox.top_right(),
8660 size: size(text_width, bounds.size.height),
8661 },
8662 HitboxBehavior::Normal,
8663 );
8664
8665 // Offset the content_bounds from the text_bounds by the gutter margin (which
8666 // is roughly half a character wide) to make hit testing work more like how we want.
8667 let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO);
8668 let content_origin = text_hitbox.origin + content_offset;
8669
8670 let height_in_lines = f64::from(bounds.size.height / line_height);
8671 let max_row = snapshot.max_point().row().as_f64();
8672
8673 // The max scroll position for the top of the window
8674 let max_scroll_top = if matches!(
8675 snapshot.mode,
8676 EditorMode::SingleLine
8677 | EditorMode::AutoHeight { .. }
8678 | EditorMode::Full {
8679 sizing_behavior: SizingBehavior::ExcludeOverscrollMargin
8680 | SizingBehavior::SizeByContent,
8681 ..
8682 }
8683 ) {
8684 (max_row - height_in_lines + 1.).max(0.)
8685 } else {
8686 let settings = EditorSettings::get_global(cx);
8687 match settings.scroll_beyond_last_line {
8688 ScrollBeyondLastLine::OnePage => max_row,
8689 ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.).max(0.),
8690 ScrollBeyondLastLine::VerticalScrollMargin => {
8691 (max_row - height_in_lines + 1. + settings.vertical_scroll_margin)
8692 .max(0.)
8693 }
8694 }
8695 };
8696
8697 let (
8698 autoscroll_request,
8699 autoscroll_containing_element,
8700 needs_horizontal_autoscroll,
8701 ) = self.editor.update(cx, |editor, cx| {
8702 let autoscroll_request = editor.scroll_manager.take_autoscroll_request();
8703
8704 let autoscroll_containing_element =
8705 autoscroll_request.is_some() || editor.has_pending_selection();
8706
8707 let (needs_horizontal_autoscroll, was_scrolled) = editor
8708 .autoscroll_vertically(
8709 bounds,
8710 line_height,
8711 max_scroll_top,
8712 autoscroll_request,
8713 window,
8714 cx,
8715 );
8716 if was_scrolled.0 {
8717 snapshot = editor.snapshot(window, cx);
8718 }
8719 (
8720 autoscroll_request,
8721 autoscroll_containing_element,
8722 needs_horizontal_autoscroll,
8723 )
8724 });
8725
8726 let mut scroll_position = snapshot.scroll_position();
8727 // The scroll position is a fractional point, the whole number of which represents
8728 // the top of the window in terms of display rows.
8729 let start_row = DisplayRow(scroll_position.y as u32);
8730 let max_row = snapshot.max_point().row();
8731 let end_row = cmp::min(
8732 (scroll_position.y + height_in_lines).ceil() as u32,
8733 max_row.next_row().0,
8734 );
8735 let end_row = DisplayRow(end_row);
8736
8737 let row_infos = snapshot
8738 .row_infos(start_row)
8739 .take((start_row..end_row).len())
8740 .collect::<Vec<RowInfo>>();
8741 let is_row_soft_wrapped = |row: usize| {
8742 row_infos
8743 .get(row)
8744 .is_none_or(|info| info.buffer_row.is_none())
8745 };
8746
8747 let start_anchor = if start_row == Default::default() {
8748 Anchor::min()
8749 } else {
8750 snapshot.buffer_snapshot().anchor_before(
8751 DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
8752 )
8753 };
8754 let end_anchor = if end_row > max_row {
8755 Anchor::max()
8756 } else {
8757 snapshot.buffer_snapshot().anchor_before(
8758 DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
8759 )
8760 };
8761
8762 let mut highlighted_rows = self
8763 .editor
8764 .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
8765
8766 let is_light = cx.theme().appearance().is_light();
8767
8768 for (ix, row_info) in row_infos.iter().enumerate() {
8769 let Some(diff_status) = row_info.diff_status else {
8770 continue;
8771 };
8772
8773 let background_color = match diff_status.kind {
8774 DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
8775 DiffHunkStatusKind::Deleted => {
8776 cx.theme().colors().version_control_deleted
8777 }
8778 DiffHunkStatusKind::Modified => {
8779 debug_panic!("modified diff status for row info");
8780 continue;
8781 }
8782 };
8783
8784 let hunk_opacity = if is_light { 0.16 } else { 0.12 };
8785
8786 let hollow_highlight = LineHighlight {
8787 background: (background_color.opacity(if is_light {
8788 0.08
8789 } else {
8790 0.06
8791 }))
8792 .into(),
8793 border: Some(if is_light {
8794 background_color.opacity(0.48)
8795 } else {
8796 background_color.opacity(0.36)
8797 }),
8798 include_gutter: true,
8799 type_id: None,
8800 };
8801
8802 let filled_highlight = LineHighlight {
8803 background: solid_background(background_color.opacity(hunk_opacity)),
8804 border: None,
8805 include_gutter: true,
8806 type_id: None,
8807 };
8808
8809 let background = if Self::diff_hunk_hollow(diff_status, cx) {
8810 hollow_highlight
8811 } else {
8812 filled_highlight
8813 };
8814
8815 highlighted_rows
8816 .entry(start_row + DisplayRow(ix as u32))
8817 .or_insert(background);
8818 }
8819
8820 let highlighted_ranges = self
8821 .editor_with_selections(cx)
8822 .map(|editor| {
8823 editor.read(cx).background_highlights_in_range(
8824 start_anchor..end_anchor,
8825 &snapshot.display_snapshot,
8826 cx.theme(),
8827 )
8828 })
8829 .unwrap_or_default();
8830 let highlighted_gutter_ranges =
8831 self.editor.read(cx).gutter_highlights_in_range(
8832 start_anchor..end_anchor,
8833 &snapshot.display_snapshot,
8834 cx,
8835 );
8836
8837 let document_colors = self
8838 .editor
8839 .read(cx)
8840 .colors
8841 .as_ref()
8842 .map(|colors| colors.editor_display_highlights(&snapshot));
8843 let redacted_ranges = self.editor.read(cx).redacted_ranges(
8844 start_anchor..end_anchor,
8845 &snapshot.display_snapshot,
8846 cx,
8847 );
8848
8849 let (local_selections, selected_buffer_ids): (
8850 Vec<Selection<Point>>,
8851 Vec<BufferId>,
8852 ) = self
8853 .editor_with_selections(cx)
8854 .map(|editor| {
8855 editor.update(cx, |editor, cx| {
8856 let all_selections =
8857 editor.selections.all::<Point>(&snapshot.display_snapshot);
8858 let selected_buffer_ids =
8859 if editor.buffer_kind(cx) == ItemBufferKind::Singleton {
8860 Vec::new()
8861 } else {
8862 let mut selected_buffer_ids =
8863 Vec::with_capacity(all_selections.len());
8864
8865 for selection in all_selections {
8866 for buffer_id in snapshot
8867 .buffer_snapshot()
8868 .buffer_ids_for_range(selection.range())
8869 {
8870 if selected_buffer_ids.last() != Some(&buffer_id) {
8871 selected_buffer_ids.push(buffer_id);
8872 }
8873 }
8874 }
8875
8876 selected_buffer_ids
8877 };
8878
8879 let mut selections = editor.selections.disjoint_in_range(
8880 start_anchor..end_anchor,
8881 &snapshot.display_snapshot,
8882 );
8883 selections
8884 .extend(editor.selections.pending(&snapshot.display_snapshot));
8885
8886 (selections, selected_buffer_ids)
8887 })
8888 })
8889 .unwrap_or_default();
8890
8891 let (selections, mut active_rows, newest_selection_head) = self
8892 .layout_selections(
8893 start_anchor,
8894 end_anchor,
8895 &local_selections,
8896 &snapshot,
8897 start_row,
8898 end_row,
8899 window,
8900 cx,
8901 );
8902 let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
8903 editor.active_breakpoints(start_row..end_row, window, cx)
8904 });
8905 for (display_row, (_, bp, state)) in &breakpoint_rows {
8906 if bp.is_enabled() && state.is_none_or(|s| s.verified) {
8907 active_rows.entry(*display_row).or_default().breakpoint = true;
8908 }
8909 }
8910
8911 let line_numbers = self.layout_line_numbers(
8912 Some(&gutter_hitbox),
8913 gutter_dimensions,
8914 line_height,
8915 scroll_position,
8916 start_row..end_row,
8917 &row_infos,
8918 &active_rows,
8919 newest_selection_head,
8920 &snapshot,
8921 window,
8922 cx,
8923 );
8924
8925 // We add the gutter breakpoint indicator to breakpoint_rows after painting
8926 // line numbers so we don't paint a line number debug accent color if a user
8927 // has their mouse over that line when a breakpoint isn't there
8928 self.editor.update(cx, |editor, _| {
8929 if let Some(phantom_breakpoint) = &mut editor
8930 .gutter_breakpoint_indicator
8931 .0
8932 .filter(|phantom_breakpoint| phantom_breakpoint.is_active)
8933 {
8934 // Is there a non-phantom breakpoint on this line?
8935 phantom_breakpoint.collides_with_existing_breakpoint = true;
8936 breakpoint_rows
8937 .entry(phantom_breakpoint.display_row)
8938 .or_insert_with(|| {
8939 let position = snapshot.display_point_to_anchor(
8940 DisplayPoint::new(phantom_breakpoint.display_row, 0),
8941 Bias::Right,
8942 );
8943 let breakpoint = Breakpoint::new_standard();
8944 phantom_breakpoint.collides_with_existing_breakpoint = false;
8945 (position, breakpoint, None)
8946 });
8947 }
8948 });
8949
8950 let mut expand_toggles =
8951 window.with_element_namespace("expand_toggles", |window| {
8952 self.layout_expand_toggles(
8953 &gutter_hitbox,
8954 gutter_dimensions,
8955 em_width,
8956 line_height,
8957 scroll_position,
8958 &row_infos,
8959 window,
8960 cx,
8961 )
8962 });
8963
8964 let mut crease_toggles =
8965 window.with_element_namespace("crease_toggles", |window| {
8966 self.layout_crease_toggles(
8967 start_row..end_row,
8968 &row_infos,
8969 &active_rows,
8970 &snapshot,
8971 window,
8972 cx,
8973 )
8974 });
8975 let crease_trailers =
8976 window.with_element_namespace("crease_trailers", |window| {
8977 self.layout_crease_trailers(
8978 row_infos.iter().copied(),
8979 &snapshot,
8980 window,
8981 cx,
8982 )
8983 });
8984
8985 let display_hunks = self.layout_gutter_diff_hunks(
8986 line_height,
8987 &gutter_hitbox,
8988 start_row..end_row,
8989 &snapshot,
8990 window,
8991 cx,
8992 );
8993
8994 let merged_highlighted_ranges =
8995 if let Some((_, colors)) = document_colors.as_ref() {
8996 &highlighted_ranges
8997 .clone()
8998 .into_iter()
8999 .chain(colors.clone())
9000 .collect()
9001 } else {
9002 &highlighted_ranges
9003 };
9004 let bg_segments_per_row = Self::bg_segments_per_row(
9005 start_row..end_row,
9006 &selections,
9007 &merged_highlighted_ranges,
9008 self.style.background,
9009 );
9010
9011 let mut line_layouts = Self::layout_lines(
9012 start_row..end_row,
9013 &snapshot,
9014 &self.style,
9015 editor_width,
9016 is_row_soft_wrapped,
9017 &bg_segments_per_row,
9018 window,
9019 cx,
9020 );
9021 let new_renderer_widths = (!is_minimap).then(|| {
9022 line_layouts
9023 .iter()
9024 .flat_map(|layout| &layout.fragments)
9025 .filter_map(|fragment| {
9026 if let LineFragment::Element { id, size, .. } = fragment {
9027 Some((*id, size.width))
9028 } else {
9029 None
9030 }
9031 })
9032 });
9033 if new_renderer_widths.is_some_and(|new_renderer_widths| {
9034 self.editor.update(cx, |editor, cx| {
9035 editor.update_renderer_widths(new_renderer_widths, cx)
9036 })
9037 }) {
9038 // If the fold widths have changed, we need to prepaint
9039 // the element again to account for any changes in
9040 // wrapping.
9041 return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
9042 }
9043
9044 let longest_line_blame_width = self
9045 .editor
9046 .update(cx, |editor, cx| {
9047 if !editor.show_git_blame_inline {
9048 return None;
9049 }
9050 let blame = editor.blame.as_ref()?;
9051 let (_, blame_entry) = blame
9052 .update(cx, |blame, cx| {
9053 let row_infos =
9054 snapshot.row_infos(snapshot.longest_row()).next()?;
9055 blame.blame_for_rows(&[row_infos], cx).next()
9056 })
9057 .flatten()?;
9058 let mut element = render_inline_blame_entry(blame_entry, style, cx)?;
9059 let inline_blame_padding =
9060 ProjectSettings::get_global(cx).git.inline_blame.padding as f32
9061 * em_advance;
9062 Some(
9063 element
9064 .layout_as_root(AvailableSpace::min_size(), window, cx)
9065 .width
9066 + inline_blame_padding,
9067 )
9068 })
9069 .unwrap_or(Pixels::ZERO);
9070
9071 let longest_line_width = layout_line(
9072 snapshot.longest_row(),
9073 &snapshot,
9074 style,
9075 editor_width,
9076 is_row_soft_wrapped,
9077 window,
9078 cx,
9079 )
9080 .width;
9081
9082 let scrollbar_layout_information = ScrollbarLayoutInformation::new(
9083 text_hitbox.bounds,
9084 glyph_grid_cell,
9085 size(
9086 longest_line_width,
9087 Pixels::from(max_row.as_f64() * f64::from(line_height)),
9088 ),
9089 longest_line_blame_width,
9090 EditorSettings::get_global(cx),
9091 );
9092
9093 let mut scroll_width = scrollbar_layout_information.scroll_range.width;
9094
9095 let sticky_header_excerpt = if snapshot.buffer_snapshot().show_headers() {
9096 snapshot.sticky_header_excerpt(scroll_position.y)
9097 } else {
9098 None
9099 };
9100 let sticky_header_excerpt_id =
9101 sticky_header_excerpt.as_ref().map(|top| top.excerpt.id);
9102
9103 let blocks = (!is_minimap)
9104 .then(|| {
9105 window.with_element_namespace("blocks", |window| {
9106 self.render_blocks(
9107 start_row..end_row,
9108 &snapshot,
9109 &hitbox,
9110 &text_hitbox,
9111 editor_width,
9112 &mut scroll_width,
9113 &editor_margins,
9114 em_width,
9115 gutter_dimensions.full_width(),
9116 line_height,
9117 &mut line_layouts,
9118 &local_selections,
9119 &selected_buffer_ids,
9120 is_row_soft_wrapped,
9121 sticky_header_excerpt_id,
9122 window,
9123 cx,
9124 )
9125 })
9126 })
9127 .unwrap_or_else(|| Ok((Vec::default(), HashMap::default())));
9128 let (mut blocks, row_block_types) = match blocks {
9129 Ok(blocks) => blocks,
9130 Err(resized_blocks) => {
9131 self.editor.update(cx, |editor, cx| {
9132 editor.resize_blocks(
9133 resized_blocks,
9134 autoscroll_request.map(|(autoscroll, _)| autoscroll),
9135 cx,
9136 )
9137 });
9138 return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
9139 }
9140 };
9141
9142 let sticky_buffer_header = sticky_header_excerpt.map(|sticky_header_excerpt| {
9143 window.with_element_namespace("blocks", |window| {
9144 self.layout_sticky_buffer_header(
9145 sticky_header_excerpt,
9146 scroll_position,
9147 line_height,
9148 right_margin,
9149 &snapshot,
9150 &hitbox,
9151 &selected_buffer_ids,
9152 &blocks,
9153 window,
9154 cx,
9155 )
9156 })
9157 });
9158
9159 let start_buffer_row =
9160 MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot()).row);
9161 let end_buffer_row =
9162 MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot()).row);
9163
9164 let scroll_max: gpui::Point<ScrollPixelOffset> = point(
9165 ScrollPixelOffset::from(
9166 ((scroll_width - editor_width) / em_advance).max(0.0),
9167 ),
9168 max_scroll_top,
9169 );
9170
9171 self.editor.update(cx, |editor, cx| {
9172 if editor.scroll_manager.clamp_scroll_left(scroll_max.x) {
9173 scroll_position.x = scroll_position.x.min(scroll_max.x);
9174 }
9175
9176 if needs_horizontal_autoscroll.0
9177 && let Some(new_scroll_position) = editor.autoscroll_horizontally(
9178 start_row,
9179 editor_width,
9180 scroll_width,
9181 em_advance,
9182 &line_layouts,
9183 autoscroll_request,
9184 window,
9185 cx,
9186 )
9187 {
9188 scroll_position = new_scroll_position;
9189 }
9190 });
9191
9192 let scroll_pixel_position = point(
9193 scroll_position.x * f64::from(em_advance),
9194 scroll_position.y * f64::from(line_height),
9195 );
9196 let indent_guides = self.layout_indent_guides(
9197 content_origin,
9198 text_hitbox.origin,
9199 start_buffer_row..end_buffer_row,
9200 scroll_pixel_position,
9201 line_height,
9202 &snapshot,
9203 window,
9204 cx,
9205 );
9206
9207 let crease_trailers =
9208 window.with_element_namespace("crease_trailers", |window| {
9209 self.prepaint_crease_trailers(
9210 crease_trailers,
9211 &line_layouts,
9212 line_height,
9213 content_origin,
9214 scroll_pixel_position,
9215 em_width,
9216 window,
9217 cx,
9218 )
9219 });
9220
9221 let (edit_prediction_popover, edit_prediction_popover_origin) = self
9222 .editor
9223 .update(cx, |editor, cx| {
9224 editor.render_edit_prediction_popover(
9225 &text_hitbox.bounds,
9226 content_origin,
9227 right_margin,
9228 &snapshot,
9229 start_row..end_row,
9230 scroll_position.y,
9231 scroll_position.y + height_in_lines,
9232 &line_layouts,
9233 line_height,
9234 scroll_position,
9235 scroll_pixel_position,
9236 newest_selection_head,
9237 editor_width,
9238 style,
9239 window,
9240 cx,
9241 )
9242 })
9243 .unzip();
9244
9245 let mut inline_diagnostics = self.layout_inline_diagnostics(
9246 &line_layouts,
9247 &crease_trailers,
9248 &row_block_types,
9249 content_origin,
9250 scroll_position,
9251 scroll_pixel_position,
9252 edit_prediction_popover_origin,
9253 start_row,
9254 end_row,
9255 line_height,
9256 em_width,
9257 style,
9258 window,
9259 cx,
9260 );
9261
9262 let mut inline_blame_layout = None;
9263 let mut inline_code_actions = None;
9264 if let Some(newest_selection_head) = newest_selection_head {
9265 let display_row = newest_selection_head.row();
9266 if (start_row..end_row).contains(&display_row)
9267 && !row_block_types.contains_key(&display_row)
9268 {
9269 inline_code_actions = self.layout_inline_code_actions(
9270 newest_selection_head,
9271 content_origin,
9272 scroll_position,
9273 scroll_pixel_position,
9274 line_height,
9275 &snapshot,
9276 window,
9277 cx,
9278 );
9279
9280 let line_ix = display_row.minus(start_row) as usize;
9281 if let (Some(row_info), Some(line_layout), Some(crease_trailer)) = (
9282 row_infos.get(line_ix),
9283 line_layouts.get(line_ix),
9284 crease_trailers.get(line_ix),
9285 ) {
9286 let crease_trailer_layout = crease_trailer.as_ref();
9287 if let Some(layout) = self.layout_inline_blame(
9288 display_row,
9289 row_info,
9290 line_layout,
9291 crease_trailer_layout,
9292 em_width,
9293 content_origin,
9294 scroll_position,
9295 scroll_pixel_position,
9296 line_height,
9297 &text_hitbox,
9298 window,
9299 cx,
9300 ) {
9301 inline_blame_layout = Some(layout);
9302 // Blame overrides inline diagnostics
9303 inline_diagnostics.remove(&display_row);
9304 }
9305 } else {
9306 log::error!(
9307 "bug: line_ix {} is out of bounds - row_infos.len(): {}, \
9308 line_layouts.len(): {}, \
9309 crease_trailers.len(): {}",
9310 line_ix,
9311 row_infos.len(),
9312 line_layouts.len(),
9313 crease_trailers.len(),
9314 );
9315 }
9316 }
9317 }
9318
9319 let blamed_display_rows = self.layout_blame_entries(
9320 &row_infos,
9321 em_width,
9322 scroll_position,
9323 line_height,
9324 &gutter_hitbox,
9325 gutter_dimensions.git_blame_entries_width,
9326 window,
9327 cx,
9328 );
9329
9330 let line_elements = self.prepaint_lines(
9331 start_row,
9332 &mut line_layouts,
9333 line_height,
9334 scroll_position,
9335 scroll_pixel_position,
9336 content_origin,
9337 window,
9338 cx,
9339 );
9340
9341 window.with_element_namespace("blocks", |window| {
9342 self.layout_blocks(
9343 &mut blocks,
9344 &hitbox,
9345 line_height,
9346 scroll_position,
9347 scroll_pixel_position,
9348 window,
9349 cx,
9350 );
9351 });
9352
9353 let cursors = self.collect_cursors(&snapshot, cx);
9354 let visible_row_range = start_row..end_row;
9355 let non_visible_cursors = cursors
9356 .iter()
9357 .any(|c| !visible_row_range.contains(&c.0.row()));
9358
9359 let visible_cursors = self.layout_visible_cursors(
9360 &snapshot,
9361 &selections,
9362 &row_block_types,
9363 start_row..end_row,
9364 &line_layouts,
9365 &text_hitbox,
9366 content_origin,
9367 scroll_position,
9368 scroll_pixel_position,
9369 line_height,
9370 em_width,
9371 em_advance,
9372 autoscroll_containing_element,
9373 window,
9374 cx,
9375 );
9376
9377 let scrollbars_layout = self.layout_scrollbars(
9378 &snapshot,
9379 &scrollbar_layout_information,
9380 content_offset,
9381 scroll_position,
9382 non_visible_cursors,
9383 right_margin,
9384 editor_width,
9385 window,
9386 cx,
9387 );
9388
9389 let gutter_settings = EditorSettings::get_global(cx).gutter;
9390
9391 let context_menu_layout =
9392 if let Some(newest_selection_head) = newest_selection_head {
9393 let newest_selection_point =
9394 newest_selection_head.to_point(&snapshot.display_snapshot);
9395 if (start_row..end_row).contains(&newest_selection_head.row()) {
9396 self.layout_cursor_popovers(
9397 line_height,
9398 &text_hitbox,
9399 content_origin,
9400 right_margin,
9401 start_row,
9402 scroll_pixel_position,
9403 &line_layouts,
9404 newest_selection_head,
9405 newest_selection_point,
9406 style,
9407 window,
9408 cx,
9409 )
9410 } else {
9411 None
9412 }
9413 } else {
9414 None
9415 };
9416
9417 self.layout_gutter_menu(
9418 line_height,
9419 &text_hitbox,
9420 content_origin,
9421 right_margin,
9422 scroll_pixel_position,
9423 gutter_dimensions.width - gutter_dimensions.left_padding,
9424 window,
9425 cx,
9426 );
9427
9428 let test_indicators = if gutter_settings.runnables {
9429 self.layout_run_indicators(
9430 line_height,
9431 start_row..end_row,
9432 &row_infos,
9433 scroll_position,
9434 &gutter_dimensions,
9435 &gutter_hitbox,
9436 &display_hunks,
9437 &snapshot,
9438 &mut breakpoint_rows,
9439 window,
9440 cx,
9441 )
9442 } else {
9443 Vec::new()
9444 };
9445
9446 let show_breakpoints = snapshot
9447 .show_breakpoints
9448 .unwrap_or(gutter_settings.breakpoints);
9449 let breakpoints = if show_breakpoints {
9450 self.layout_breakpoints(
9451 line_height,
9452 start_row..end_row,
9453 scroll_position,
9454 &gutter_dimensions,
9455 &gutter_hitbox,
9456 &display_hunks,
9457 &snapshot,
9458 breakpoint_rows,
9459 &row_infos,
9460 window,
9461 cx,
9462 )
9463 } else {
9464 Vec::new()
9465 };
9466
9467 self.layout_signature_help(
9468 &hitbox,
9469 content_origin,
9470 scroll_pixel_position,
9471 newest_selection_head,
9472 start_row,
9473 &line_layouts,
9474 line_height,
9475 em_width,
9476 context_menu_layout,
9477 window,
9478 cx,
9479 );
9480
9481 if !cx.has_active_drag() {
9482 self.layout_hover_popovers(
9483 &snapshot,
9484 &hitbox,
9485 start_row..end_row,
9486 content_origin,
9487 scroll_pixel_position,
9488 &line_layouts,
9489 line_height,
9490 em_width,
9491 context_menu_layout,
9492 window,
9493 cx,
9494 );
9495 }
9496
9497 let mouse_context_menu = self.layout_mouse_context_menu(
9498 &snapshot,
9499 start_row..end_row,
9500 content_origin,
9501 window,
9502 cx,
9503 );
9504
9505 window.with_element_namespace("crease_toggles", |window| {
9506 self.prepaint_crease_toggles(
9507 &mut crease_toggles,
9508 line_height,
9509 &gutter_dimensions,
9510 gutter_settings,
9511 scroll_pixel_position,
9512 &gutter_hitbox,
9513 window,
9514 cx,
9515 )
9516 });
9517
9518 window.with_element_namespace("expand_toggles", |window| {
9519 self.prepaint_expand_toggles(&mut expand_toggles, window, cx)
9520 });
9521
9522 let wrap_guides = self.layout_wrap_guides(
9523 em_advance,
9524 scroll_position,
9525 content_origin,
9526 scrollbars_layout.as_ref(),
9527 vertical_scrollbar_width,
9528 &hitbox,
9529 window,
9530 cx,
9531 );
9532
9533 let minimap = window.with_element_namespace("minimap", |window| {
9534 self.layout_minimap(
9535 &snapshot,
9536 minimap_width,
9537 scroll_position,
9538 &scrollbar_layout_information,
9539 scrollbars_layout.as_ref(),
9540 window,
9541 cx,
9542 )
9543 });
9544
9545 let invisible_symbol_font_size = font_size / 2.;
9546 let whitespace_map = &self
9547 .editor
9548 .read(cx)
9549 .buffer
9550 .read(cx)
9551 .language_settings(cx)
9552 .whitespace_map;
9553
9554 let tab_char = whitespace_map.tab.clone();
9555 let tab_len = tab_char.len();
9556 let tab_invisible = window.text_system().shape_line(
9557 tab_char,
9558 invisible_symbol_font_size,
9559 &[TextRun {
9560 len: tab_len,
9561 font: self.style.text.font(),
9562 color: cx.theme().colors().editor_invisible,
9563 ..Default::default()
9564 }],
9565 None,
9566 );
9567
9568 let space_char = whitespace_map.space.clone();
9569 let space_len = space_char.len();
9570 let space_invisible = window.text_system().shape_line(
9571 space_char,
9572 invisible_symbol_font_size,
9573 &[TextRun {
9574 len: space_len,
9575 font: self.style.text.font(),
9576 color: cx.theme().colors().editor_invisible,
9577 ..Default::default()
9578 }],
9579 None,
9580 );
9581
9582 let mode = snapshot.mode.clone();
9583
9584 let (diff_hunk_controls, diff_hunk_control_bounds) = if is_read_only {
9585 (vec![], vec![])
9586 } else {
9587 self.layout_diff_hunk_controls(
9588 start_row..end_row,
9589 &row_infos,
9590 &text_hitbox,
9591 newest_selection_head,
9592 line_height,
9593 right_margin,
9594 scroll_pixel_position,
9595 &display_hunks,
9596 &highlighted_rows,
9597 self.editor.clone(),
9598 window,
9599 cx,
9600 )
9601 };
9602
9603 let position_map = Rc::new(PositionMap {
9604 size: bounds.size,
9605 visible_row_range,
9606 scroll_position,
9607 scroll_pixel_position,
9608 scroll_max,
9609 line_layouts,
9610 line_height,
9611 em_width,
9612 em_advance,
9613 snapshot,
9614 gutter_hitbox: gutter_hitbox.clone(),
9615 text_hitbox: text_hitbox.clone(),
9616 inline_blame_bounds: inline_blame_layout
9617 .as_ref()
9618 .map(|layout| (layout.bounds, layout.buffer_id, layout.entry.clone())),
9619 display_hunks: display_hunks.clone(),
9620 diff_hunk_control_bounds,
9621 });
9622
9623 self.editor.update(cx, |editor, _| {
9624 editor.last_position_map = Some(position_map.clone())
9625 });
9626
9627 EditorLayout {
9628 mode,
9629 position_map,
9630 visible_display_row_range: start_row..end_row,
9631 wrap_guides,
9632 indent_guides,
9633 hitbox,
9634 gutter_hitbox,
9635 display_hunks,
9636 content_origin,
9637 scrollbars_layout,
9638 minimap,
9639 active_rows,
9640 highlighted_rows,
9641 highlighted_ranges,
9642 highlighted_gutter_ranges,
9643 redacted_ranges,
9644 document_colors,
9645 line_elements,
9646 line_numbers,
9647 blamed_display_rows,
9648 inline_diagnostics,
9649 inline_blame_layout,
9650 inline_code_actions,
9651 blocks,
9652 cursors,
9653 visible_cursors,
9654 selections,
9655 edit_prediction_popover,
9656 diff_hunk_controls,
9657 mouse_context_menu,
9658 test_indicators,
9659 breakpoints,
9660 crease_toggles,
9661 crease_trailers,
9662 tab_invisible,
9663 space_invisible,
9664 sticky_buffer_header,
9665 expand_toggles,
9666 }
9667 })
9668 })
9669 })
9670 }
9671
9672 fn paint(
9673 &mut self,
9674 _: Option<&GlobalElementId>,
9675 _inspector_id: Option<&gpui::InspectorElementId>,
9676 bounds: Bounds<gpui::Pixels>,
9677 _: &mut Self::RequestLayoutState,
9678 layout: &mut Self::PrepaintState,
9679 window: &mut Window,
9680 cx: &mut App,
9681 ) {
9682 if !layout.mode.is_minimap() {
9683 let focus_handle = self.editor.focus_handle(cx);
9684 let key_context = self
9685 .editor
9686 .update(cx, |editor, cx| editor.key_context(window, cx));
9687
9688 window.set_key_context(key_context);
9689 window.handle_input(
9690 &focus_handle,
9691 ElementInputHandler::new(bounds, self.editor.clone()),
9692 cx,
9693 );
9694 self.register_actions(window, cx);
9695 self.register_key_listeners(window, cx, layout);
9696 }
9697
9698 let text_style = TextStyleRefinement {
9699 font_size: Some(self.style.text.font_size),
9700 line_height: Some(self.style.text.line_height),
9701 ..Default::default()
9702 };
9703 let rem_size = self.rem_size(cx);
9704 window.with_rem_size(rem_size, |window| {
9705 window.with_text_style(Some(text_style), |window| {
9706 window.with_content_mask(Some(ContentMask { bounds }), |window| {
9707 self.paint_mouse_listeners(layout, window, cx);
9708 self.paint_background(layout, window, cx);
9709 self.paint_indent_guides(layout, window, cx);
9710
9711 if layout.gutter_hitbox.size.width > Pixels::ZERO {
9712 self.paint_blamed_display_rows(layout, window, cx);
9713 self.paint_line_numbers(layout, window, cx);
9714 }
9715
9716 self.paint_text(layout, window, cx);
9717
9718 if layout.gutter_hitbox.size.width > Pixels::ZERO {
9719 self.paint_gutter_highlights(layout, window, cx);
9720 self.paint_gutter_indicators(layout, window, cx);
9721 }
9722
9723 if !layout.blocks.is_empty() {
9724 window.with_element_namespace("blocks", |window| {
9725 self.paint_blocks(layout, window, cx);
9726 });
9727 }
9728
9729 window.with_element_namespace("blocks", |window| {
9730 if let Some(mut sticky_header) = layout.sticky_buffer_header.take() {
9731 sticky_header.paint(window, cx)
9732 }
9733 });
9734
9735 self.paint_minimap(layout, window, cx);
9736 self.paint_scrollbars(layout, window, cx);
9737 self.paint_edit_prediction_popover(layout, window, cx);
9738 self.paint_mouse_context_menu(layout, window, cx);
9739 });
9740 })
9741 })
9742 }
9743}
9744
9745pub(super) fn gutter_bounds(
9746 editor_bounds: Bounds<Pixels>,
9747 gutter_dimensions: GutterDimensions,
9748) -> Bounds<Pixels> {
9749 Bounds {
9750 origin: editor_bounds.origin,
9751 size: size(gutter_dimensions.width, editor_bounds.size.height),
9752 }
9753}
9754
9755#[derive(Clone, Copy)]
9756struct ContextMenuLayout {
9757 y_flipped: bool,
9758 bounds: Bounds<Pixels>,
9759}
9760
9761/// Holds information required for layouting the editor scrollbars.
9762struct ScrollbarLayoutInformation {
9763 /// The bounds of the editor area (excluding the content offset).
9764 editor_bounds: Bounds<Pixels>,
9765 /// The available range to scroll within the document.
9766 scroll_range: Size<Pixels>,
9767 /// The space available for one glyph in the editor.
9768 glyph_grid_cell: Size<Pixels>,
9769}
9770
9771impl ScrollbarLayoutInformation {
9772 pub fn new(
9773 editor_bounds: Bounds<Pixels>,
9774 glyph_grid_cell: Size<Pixels>,
9775 document_size: Size<Pixels>,
9776 longest_line_blame_width: Pixels,
9777 settings: &EditorSettings,
9778 ) -> Self {
9779 let vertical_overscroll = match settings.scroll_beyond_last_line {
9780 ScrollBeyondLastLine::OnePage => editor_bounds.size.height,
9781 ScrollBeyondLastLine::Off => glyph_grid_cell.height,
9782 ScrollBeyondLastLine::VerticalScrollMargin => {
9783 (1.0 + settings.vertical_scroll_margin) as f32 * glyph_grid_cell.height
9784 }
9785 };
9786
9787 let overscroll = size(longest_line_blame_width, vertical_overscroll);
9788
9789 ScrollbarLayoutInformation {
9790 editor_bounds,
9791 scroll_range: document_size + overscroll,
9792 glyph_grid_cell,
9793 }
9794 }
9795}
9796
9797impl IntoElement for EditorElement {
9798 type Element = Self;
9799
9800 fn into_element(self) -> Self::Element {
9801 self
9802 }
9803}
9804
9805pub struct EditorLayout {
9806 position_map: Rc<PositionMap>,
9807 hitbox: Hitbox,
9808 gutter_hitbox: Hitbox,
9809 content_origin: gpui::Point<Pixels>,
9810 scrollbars_layout: Option<EditorScrollbars>,
9811 minimap: Option<MinimapLayout>,
9812 mode: EditorMode,
9813 wrap_guides: SmallVec<[(Pixels, bool); 2]>,
9814 indent_guides: Option<Vec<IndentGuideLayout>>,
9815 visible_display_row_range: Range<DisplayRow>,
9816 active_rows: BTreeMap<DisplayRow, LineHighlightSpec>,
9817 highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
9818 line_elements: SmallVec<[AnyElement; 1]>,
9819 line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
9820 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
9821 blamed_display_rows: Option<Vec<AnyElement>>,
9822 inline_diagnostics: HashMap<DisplayRow, AnyElement>,
9823 inline_blame_layout: Option<InlineBlameLayout>,
9824 inline_code_actions: Option<AnyElement>,
9825 blocks: Vec<BlockLayout>,
9826 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
9827 highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
9828 redacted_ranges: Vec<Range<DisplayPoint>>,
9829 cursors: Vec<(DisplayPoint, Hsla)>,
9830 visible_cursors: Vec<CursorLayout>,
9831 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
9832 test_indicators: Vec<AnyElement>,
9833 breakpoints: Vec<AnyElement>,
9834 crease_toggles: Vec<Option<AnyElement>>,
9835 expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,
9836 diff_hunk_controls: Vec<AnyElement>,
9837 crease_trailers: Vec<Option<CreaseTrailerLayout>>,
9838 edit_prediction_popover: Option<AnyElement>,
9839 mouse_context_menu: Option<AnyElement>,
9840 tab_invisible: ShapedLine,
9841 space_invisible: ShapedLine,
9842 sticky_buffer_header: Option<AnyElement>,
9843 document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
9844}
9845
9846impl EditorLayout {
9847 fn line_end_overshoot(&self) -> Pixels {
9848 0.15 * self.position_map.line_height
9849 }
9850}
9851
9852#[derive(Debug)]
9853struct LineNumberSegment {
9854 shaped_line: ShapedLine,
9855 hitbox: Option<Hitbox>,
9856}
9857
9858#[derive(Debug)]
9859struct LineNumberLayout {
9860 segments: SmallVec<[LineNumberSegment; 1]>,
9861}
9862
9863struct ColoredRange<T> {
9864 start: T,
9865 end: T,
9866 color: Hsla,
9867}
9868
9869impl Along for ScrollbarAxes {
9870 type Unit = bool;
9871
9872 fn along(&self, axis: ScrollbarAxis) -> Self::Unit {
9873 match axis {
9874 ScrollbarAxis::Horizontal => self.horizontal,
9875 ScrollbarAxis::Vertical => self.vertical,
9876 }
9877 }
9878
9879 fn apply_along(&self, axis: ScrollbarAxis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self {
9880 match axis {
9881 ScrollbarAxis::Horizontal => ScrollbarAxes {
9882 horizontal: f(self.horizontal),
9883 vertical: self.vertical,
9884 },
9885 ScrollbarAxis::Vertical => ScrollbarAxes {
9886 horizontal: self.horizontal,
9887 vertical: f(self.vertical),
9888 },
9889 }
9890 }
9891}
9892
9893#[derive(Clone)]
9894struct EditorScrollbars {
9895 pub vertical: Option<ScrollbarLayout>,
9896 pub horizontal: Option<ScrollbarLayout>,
9897 pub visible: bool,
9898}
9899
9900impl EditorScrollbars {
9901 pub fn from_scrollbar_axes(
9902 show_scrollbar: ScrollbarAxes,
9903 layout_information: &ScrollbarLayoutInformation,
9904 content_offset: gpui::Point<Pixels>,
9905 scroll_position: gpui::Point<f64>,
9906 scrollbar_width: Pixels,
9907 right_margin: Pixels,
9908 editor_width: Pixels,
9909 show_scrollbars: bool,
9910 scrollbar_state: Option<&ActiveScrollbarState>,
9911 window: &mut Window,
9912 ) -> Self {
9913 let ScrollbarLayoutInformation {
9914 editor_bounds,
9915 scroll_range,
9916 glyph_grid_cell,
9917 } = layout_information;
9918
9919 let viewport_size = size(editor_width, editor_bounds.size.height);
9920
9921 let scrollbar_bounds_for = |axis: ScrollbarAxis| match axis {
9922 ScrollbarAxis::Horizontal => Bounds::from_corner_and_size(
9923 Corner::BottomLeft,
9924 editor_bounds.bottom_left(),
9925 size(
9926 // The horizontal viewport size differs from the space available for the
9927 // horizontal scrollbar, so we have to manually stitch it together here.
9928 editor_bounds.size.width - right_margin,
9929 scrollbar_width,
9930 ),
9931 ),
9932 ScrollbarAxis::Vertical => Bounds::from_corner_and_size(
9933 Corner::TopRight,
9934 editor_bounds.top_right(),
9935 size(scrollbar_width, viewport_size.height),
9936 ),
9937 };
9938
9939 let mut create_scrollbar_layout = |axis| {
9940 let viewport_size = viewport_size.along(axis);
9941 let scroll_range = scroll_range.along(axis);
9942
9943 // We always want a vertical scrollbar track for scrollbar diagnostic visibility.
9944 (show_scrollbar.along(axis)
9945 && (axis == ScrollbarAxis::Vertical || scroll_range > viewport_size))
9946 .then(|| {
9947 ScrollbarLayout::new(
9948 window.insert_hitbox(scrollbar_bounds_for(axis), HitboxBehavior::Normal),
9949 viewport_size,
9950 scroll_range,
9951 glyph_grid_cell.along(axis),
9952 content_offset.along(axis),
9953 scroll_position.along(axis),
9954 show_scrollbars,
9955 axis,
9956 )
9957 .with_thumb_state(
9958 scrollbar_state.and_then(|state| state.thumb_state_for_axis(axis)),
9959 )
9960 })
9961 };
9962
9963 Self {
9964 vertical: create_scrollbar_layout(ScrollbarAxis::Vertical),
9965 horizontal: create_scrollbar_layout(ScrollbarAxis::Horizontal),
9966 visible: show_scrollbars,
9967 }
9968 }
9969
9970 pub fn iter_scrollbars(&self) -> impl Iterator<Item = (&ScrollbarLayout, ScrollbarAxis)> + '_ {
9971 [
9972 (&self.vertical, ScrollbarAxis::Vertical),
9973 (&self.horizontal, ScrollbarAxis::Horizontal),
9974 ]
9975 .into_iter()
9976 .filter_map(|(scrollbar, axis)| scrollbar.as_ref().map(|s| (s, axis)))
9977 }
9978
9979 /// Returns the currently hovered scrollbar axis, if any.
9980 pub fn get_hovered_axis(&self, window: &Window) -> Option<(&ScrollbarLayout, ScrollbarAxis)> {
9981 self.iter_scrollbars()
9982 .find(|s| s.0.hitbox.is_hovered(window))
9983 }
9984}
9985
9986#[derive(Clone)]
9987struct ScrollbarLayout {
9988 hitbox: Hitbox,
9989 visible_range: Range<ScrollOffset>,
9990 text_unit_size: Pixels,
9991 thumb_bounds: Option<Bounds<Pixels>>,
9992 thumb_state: ScrollbarThumbState,
9993}
9994
9995impl ScrollbarLayout {
9996 const BORDER_WIDTH: Pixels = px(1.0);
9997 const LINE_MARKER_HEIGHT: Pixels = px(2.0);
9998 const MIN_MARKER_HEIGHT: Pixels = px(5.0);
9999 const MIN_THUMB_SIZE: Pixels = px(25.0);
10000
10001 fn new(
10002 scrollbar_track_hitbox: Hitbox,
10003 viewport_size: Pixels,
10004 scroll_range: Pixels,
10005 glyph_space: Pixels,
10006 content_offset: Pixels,
10007 scroll_position: ScrollOffset,
10008 show_thumb: bool,
10009 axis: ScrollbarAxis,
10010 ) -> Self {
10011 let track_bounds = scrollbar_track_hitbox.bounds;
10012 // The length of the track available to the scrollbar thumb. We deliberately
10013 // exclude the content size here so that the thumb aligns with the content.
10014 let track_length = track_bounds.size.along(axis) - content_offset;
10015
10016 Self::new_with_hitbox_and_track_length(
10017 scrollbar_track_hitbox,
10018 track_length,
10019 viewport_size,
10020 scroll_range.into(),
10021 glyph_space,
10022 content_offset.into(),
10023 scroll_position,
10024 show_thumb,
10025 axis,
10026 )
10027 }
10028
10029 fn for_minimap(
10030 minimap_track_hitbox: Hitbox,
10031 visible_lines: f64,
10032 total_editor_lines: f64,
10033 minimap_line_height: Pixels,
10034 scroll_position: ScrollOffset,
10035 minimap_scroll_top: ScrollOffset,
10036 show_thumb: bool,
10037 ) -> Self {
10038 // The scrollbar thumb size is calculated as
10039 // (visible_content/total_content) Γ scrollbar_track_length.
10040 //
10041 // For the minimap's thumb layout, we leverage this by setting the
10042 // scrollbar track length to the entire document size (using minimap line
10043 // height). This creates a thumb that exactly represents the editor
10044 // viewport scaled to minimap proportions.
10045 //
10046 // We adjust the thumb position relative to `minimap_scroll_top` to
10047 // accommodate for the deliberately oversized track.
10048 //
10049 // This approach ensures that the minimap thumb accurately reflects the
10050 // editor's current scroll position whilst nicely synchronizing the minimap
10051 // thumb and scrollbar thumb.
10052 let scroll_range = total_editor_lines * f64::from(minimap_line_height);
10053 let viewport_size = visible_lines * f64::from(minimap_line_height);
10054
10055 let track_top_offset = -minimap_scroll_top * f64::from(minimap_line_height);
10056
10057 Self::new_with_hitbox_and_track_length(
10058 minimap_track_hitbox,
10059 Pixels::from(scroll_range),
10060 Pixels::from(viewport_size),
10061 scroll_range,
10062 minimap_line_height,
10063 track_top_offset,
10064 scroll_position,
10065 show_thumb,
10066 ScrollbarAxis::Vertical,
10067 )
10068 }
10069
10070 fn new_with_hitbox_and_track_length(
10071 scrollbar_track_hitbox: Hitbox,
10072 track_length: Pixels,
10073 viewport_size: Pixels,
10074 scroll_range: f64,
10075 glyph_space: Pixels,
10076 content_offset: ScrollOffset,
10077 scroll_position: ScrollOffset,
10078 show_thumb: bool,
10079 axis: ScrollbarAxis,
10080 ) -> Self {
10081 let text_units_per_page = f64::from(viewport_size / glyph_space);
10082 let visible_range = scroll_position..scroll_position + text_units_per_page;
10083 let total_text_units = scroll_range / f64::from(glyph_space);
10084
10085 let thumb_percentage = text_units_per_page / total_text_units;
10086 let thumb_size = Pixels::from(ScrollOffset::from(track_length) * thumb_percentage)
10087 .max(ScrollbarLayout::MIN_THUMB_SIZE)
10088 .min(track_length);
10089
10090 let text_unit_divisor = (total_text_units - text_units_per_page).max(0.);
10091
10092 let content_larger_than_viewport = text_unit_divisor > 0.;
10093
10094 let text_unit_size = if content_larger_than_viewport {
10095 Pixels::from(ScrollOffset::from(track_length - thumb_size) / text_unit_divisor)
10096 } else {
10097 glyph_space
10098 };
10099
10100 let thumb_bounds = (show_thumb && content_larger_than_viewport).then(|| {
10101 Self::thumb_bounds(
10102 &scrollbar_track_hitbox,
10103 content_offset,
10104 visible_range.start,
10105 text_unit_size,
10106 thumb_size,
10107 axis,
10108 )
10109 });
10110
10111 ScrollbarLayout {
10112 hitbox: scrollbar_track_hitbox,
10113 visible_range,
10114 text_unit_size,
10115 thumb_bounds,
10116 thumb_state: Default::default(),
10117 }
10118 }
10119
10120 fn with_thumb_state(self, thumb_state: Option<ScrollbarThumbState>) -> Self {
10121 if let Some(thumb_state) = thumb_state {
10122 Self {
10123 thumb_state,
10124 ..self
10125 }
10126 } else {
10127 self
10128 }
10129 }
10130
10131 fn thumb_bounds(
10132 scrollbar_track: &Hitbox,
10133 content_offset: f64,
10134 visible_range_start: f64,
10135 text_unit_size: Pixels,
10136 thumb_size: Pixels,
10137 axis: ScrollbarAxis,
10138 ) -> Bounds<Pixels> {
10139 let thumb_origin = scrollbar_track.origin.apply_along(axis, |origin| {
10140 origin
10141 + Pixels::from(
10142 content_offset + visible_range_start * ScrollOffset::from(text_unit_size),
10143 )
10144 });
10145 Bounds::new(
10146 thumb_origin,
10147 scrollbar_track.size.apply_along(axis, |_| thumb_size),
10148 )
10149 }
10150
10151 fn thumb_hovered(&self, position: &gpui::Point<Pixels>) -> bool {
10152 self.thumb_bounds
10153 .is_some_and(|bounds| bounds.contains(position))
10154 }
10155
10156 fn marker_quads_for_ranges(
10157 &self,
10158 row_ranges: impl IntoIterator<Item = ColoredRange<DisplayRow>>,
10159 column: Option<usize>,
10160 ) -> Vec<PaintQuad> {
10161 struct MinMax {
10162 min: Pixels,
10163 max: Pixels,
10164 }
10165 let (x_range, height_limit) = if let Some(column) = column {
10166 let column_width = ((self.hitbox.size.width - Self::BORDER_WIDTH) / 3.0).floor();
10167 let start = Self::BORDER_WIDTH + (column as f32 * column_width);
10168 let end = start + column_width;
10169 (
10170 Range { start, end },
10171 MinMax {
10172 min: Self::MIN_MARKER_HEIGHT,
10173 max: px(f32::MAX),
10174 },
10175 )
10176 } else {
10177 (
10178 Range {
10179 start: Self::BORDER_WIDTH,
10180 end: self.hitbox.size.width,
10181 },
10182 MinMax {
10183 min: Self::LINE_MARKER_HEIGHT,
10184 max: Self::LINE_MARKER_HEIGHT,
10185 },
10186 )
10187 };
10188
10189 let row_to_y = |row: DisplayRow| row.as_f64() as f32 * self.text_unit_size;
10190 let mut pixel_ranges = row_ranges
10191 .into_iter()
10192 .map(|range| {
10193 let start_y = row_to_y(range.start);
10194 let end_y = row_to_y(range.end)
10195 + self
10196 .text_unit_size
10197 .max(height_limit.min)
10198 .min(height_limit.max);
10199 ColoredRange {
10200 start: start_y,
10201 end: end_y,
10202 color: range.color,
10203 }
10204 })
10205 .peekable();
10206
10207 let mut quads = Vec::new();
10208 while let Some(mut pixel_range) = pixel_ranges.next() {
10209 while let Some(next_pixel_range) = pixel_ranges.peek() {
10210 if pixel_range.end >= next_pixel_range.start - px(1.0)
10211 && pixel_range.color == next_pixel_range.color
10212 {
10213 pixel_range.end = next_pixel_range.end.max(pixel_range.end);
10214 pixel_ranges.next();
10215 } else {
10216 break;
10217 }
10218 }
10219
10220 let bounds = Bounds::from_corners(
10221 point(x_range.start, pixel_range.start),
10222 point(x_range.end, pixel_range.end),
10223 );
10224 quads.push(quad(
10225 bounds,
10226 Corners::default(),
10227 pixel_range.color,
10228 Edges::default(),
10229 Hsla::transparent_black(),
10230 BorderStyle::default(),
10231 ));
10232 }
10233
10234 quads
10235 }
10236}
10237
10238struct MinimapLayout {
10239 pub minimap: AnyElement,
10240 pub thumb_layout: ScrollbarLayout,
10241 pub minimap_scroll_top: ScrollOffset,
10242 pub minimap_line_height: Pixels,
10243 pub thumb_border_style: MinimapThumbBorder,
10244 pub max_scroll_top: ScrollOffset,
10245}
10246
10247impl MinimapLayout {
10248 /// The minimum width of the minimap in columns. If the minimap is smaller than this, it will be hidden.
10249 const MINIMAP_MIN_WIDTH_COLUMNS: f32 = 20.;
10250 /// The minimap width as a percentage of the editor width.
10251 const MINIMAP_WIDTH_PCT: f32 = 0.15;
10252 /// Calculates the scroll top offset the minimap editor has to have based on the
10253 /// current scroll progress.
10254 fn calculate_minimap_top_offset(
10255 document_lines: f64,
10256 visible_editor_lines: f64,
10257 visible_minimap_lines: f64,
10258 scroll_position: f64,
10259 ) -> ScrollOffset {
10260 let non_visible_document_lines = (document_lines - visible_editor_lines).max(0.);
10261 if non_visible_document_lines == 0. {
10262 0.
10263 } else {
10264 let scroll_percentage = (scroll_position / non_visible_document_lines).clamp(0., 1.);
10265 scroll_percentage * (document_lines - visible_minimap_lines).max(0.)
10266 }
10267 }
10268}
10269
10270struct CreaseTrailerLayout {
10271 element: AnyElement,
10272 bounds: Bounds<Pixels>,
10273}
10274
10275pub(crate) struct PositionMap {
10276 pub size: Size<Pixels>,
10277 pub line_height: Pixels,
10278 pub scroll_position: gpui::Point<ScrollOffset>,
10279 pub scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
10280 pub scroll_max: gpui::Point<ScrollOffset>,
10281 pub em_width: Pixels,
10282 pub em_advance: Pixels,
10283 pub visible_row_range: Range<DisplayRow>,
10284 pub line_layouts: Vec<LineWithInvisibles>,
10285 pub snapshot: EditorSnapshot,
10286 pub text_hitbox: Hitbox,
10287 pub gutter_hitbox: Hitbox,
10288 pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,
10289 pub display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
10290 pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds<Pixels>)>,
10291}
10292
10293#[derive(Debug, Copy, Clone)]
10294pub struct PointForPosition {
10295 pub previous_valid: DisplayPoint,
10296 pub next_valid: DisplayPoint,
10297 pub exact_unclipped: DisplayPoint,
10298 pub column_overshoot_after_line_end: u32,
10299}
10300
10301impl PointForPosition {
10302 pub fn as_valid(&self) -> Option<DisplayPoint> {
10303 if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
10304 Some(self.previous_valid)
10305 } else {
10306 None
10307 }
10308 }
10309
10310 pub fn intersects_selection(&self, selection: &Selection<DisplayPoint>) -> bool {
10311 let Some(valid_point) = self.as_valid() else {
10312 return false;
10313 };
10314 let range = selection.range();
10315
10316 let candidate_row = valid_point.row();
10317 let candidate_col = valid_point.column();
10318
10319 let start_row = range.start.row();
10320 let start_col = range.start.column();
10321 let end_row = range.end.row();
10322 let end_col = range.end.column();
10323
10324 if candidate_row < start_row || candidate_row > end_row {
10325 false
10326 } else if start_row == end_row {
10327 candidate_col >= start_col && candidate_col < end_col
10328 } else if candidate_row == start_row {
10329 candidate_col >= start_col
10330 } else if candidate_row == end_row {
10331 candidate_col < end_col
10332 } else {
10333 true
10334 }
10335 }
10336}
10337
10338impl PositionMap {
10339 pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
10340 let text_bounds = self.text_hitbox.bounds;
10341 let scroll_position = self.snapshot.scroll_position();
10342 let position = position - text_bounds.origin;
10343 let y = position.y.max(px(0.)).min(self.size.height);
10344 let x = position.x + (scroll_position.x as f32 * self.em_advance);
10345 let row = ((y / self.line_height) as f64 + scroll_position.y) as u32;
10346
10347 let (column, x_overshoot_after_line_end) = if let Some(line) = self
10348 .line_layouts
10349 .get(row as usize - scroll_position.y as usize)
10350 {
10351 if let Some(ix) = line.index_for_x(x) {
10352 (ix as u32, px(0.))
10353 } else {
10354 (line.len as u32, px(0.).max(x - line.width))
10355 }
10356 } else {
10357 (0, x)
10358 };
10359
10360 let mut exact_unclipped = DisplayPoint::new(DisplayRow(row), column);
10361 let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
10362 let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
10363
10364 let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
10365 *exact_unclipped.column_mut() += column_overshoot_after_line_end;
10366 PointForPosition {
10367 previous_valid,
10368 next_valid,
10369 exact_unclipped,
10370 column_overshoot_after_line_end,
10371 }
10372 }
10373}
10374
10375struct BlockLayout {
10376 id: BlockId,
10377 x_offset: Pixels,
10378 row: Option<DisplayRow>,
10379 element: AnyElement,
10380 available_space: Size<AvailableSpace>,
10381 style: BlockStyle,
10382 overlaps_gutter: bool,
10383 is_buffer_header: bool,
10384}
10385
10386pub fn layout_line(
10387 row: DisplayRow,
10388 snapshot: &EditorSnapshot,
10389 style: &EditorStyle,
10390 text_width: Pixels,
10391 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
10392 window: &mut Window,
10393 cx: &mut App,
10394) -> LineWithInvisibles {
10395 let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
10396 LineWithInvisibles::from_chunks(
10397 chunks,
10398 style,
10399 MAX_LINE_LEN,
10400 1,
10401 &snapshot.mode,
10402 text_width,
10403 is_row_soft_wrapped,
10404 &[],
10405 window,
10406 cx,
10407 )
10408 .pop()
10409 .unwrap()
10410}
10411
10412#[derive(Debug)]
10413pub struct IndentGuideLayout {
10414 origin: gpui::Point<Pixels>,
10415 length: Pixels,
10416 single_indent_width: Pixels,
10417 depth: u32,
10418 active: bool,
10419 settings: IndentGuideSettings,
10420}
10421
10422pub struct CursorLayout {
10423 origin: gpui::Point<Pixels>,
10424 block_width: Pixels,
10425 line_height: Pixels,
10426 color: Hsla,
10427 shape: CursorShape,
10428 block_text: Option<ShapedLine>,
10429 cursor_name: Option<AnyElement>,
10430}
10431
10432#[derive(Debug)]
10433pub struct CursorName {
10434 string: SharedString,
10435 color: Hsla,
10436 is_top_row: bool,
10437}
10438
10439impl CursorLayout {
10440 pub fn new(
10441 origin: gpui::Point<Pixels>,
10442 block_width: Pixels,
10443 line_height: Pixels,
10444 color: Hsla,
10445 shape: CursorShape,
10446 block_text: Option<ShapedLine>,
10447 ) -> CursorLayout {
10448 CursorLayout {
10449 origin,
10450 block_width,
10451 line_height,
10452 color,
10453 shape,
10454 block_text,
10455 cursor_name: None,
10456 }
10457 }
10458
10459 pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
10460 Bounds {
10461 origin: self.origin + origin,
10462 size: size(self.block_width, self.line_height),
10463 }
10464 }
10465
10466 fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
10467 match self.shape {
10468 CursorShape::Bar => Bounds {
10469 origin: self.origin + origin,
10470 size: size(px(2.0), self.line_height),
10471 },
10472 CursorShape::Block | CursorShape::Hollow => Bounds {
10473 origin: self.origin + origin,
10474 size: size(self.block_width, self.line_height),
10475 },
10476 CursorShape::Underline => Bounds {
10477 origin: self.origin
10478 + origin
10479 + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)),
10480 size: size(self.block_width, px(2.0)),
10481 },
10482 }
10483 }
10484
10485 pub fn layout(
10486 &mut self,
10487 origin: gpui::Point<Pixels>,
10488 cursor_name: Option<CursorName>,
10489 window: &mut Window,
10490 cx: &mut App,
10491 ) {
10492 if let Some(cursor_name) = cursor_name {
10493 let bounds = self.bounds(origin);
10494 let text_size = self.line_height / 1.5;
10495
10496 let name_origin = if cursor_name.is_top_row {
10497 point(bounds.right() - px(1.), bounds.top())
10498 } else {
10499 match self.shape {
10500 CursorShape::Bar => point(
10501 bounds.right() - px(2.),
10502 bounds.top() - text_size / 2. - px(1.),
10503 ),
10504 _ => point(
10505 bounds.right() - px(1.),
10506 bounds.top() - text_size / 2. - px(1.),
10507 ),
10508 }
10509 };
10510 let mut name_element = div()
10511 .bg(self.color)
10512 .text_size(text_size)
10513 .px_0p5()
10514 .line_height(text_size + px(2.))
10515 .text_color(cursor_name.color)
10516 .child(cursor_name.string)
10517 .into_any_element();
10518
10519 name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx);
10520
10521 self.cursor_name = Some(name_element);
10522 }
10523 }
10524
10525 pub fn paint(&mut self, origin: gpui::Point<Pixels>, window: &mut Window, cx: &mut App) {
10526 let bounds = self.bounds(origin);
10527
10528 //Draw background or border quad
10529 let cursor = if matches!(self.shape, CursorShape::Hollow) {
10530 outline(bounds, self.color, BorderStyle::Solid)
10531 } else {
10532 fill(bounds, self.color)
10533 };
10534
10535 if let Some(name) = &mut self.cursor_name {
10536 name.paint(window, cx);
10537 }
10538
10539 window.paint_quad(cursor);
10540
10541 if let Some(block_text) = &self.block_text {
10542 block_text
10543 .paint(self.origin + origin, self.line_height, window, cx)
10544 .log_err();
10545 }
10546 }
10547
10548 pub fn shape(&self) -> CursorShape {
10549 self.shape
10550 }
10551}
10552
10553#[derive(Debug)]
10554pub struct HighlightedRange {
10555 pub start_y: Pixels,
10556 pub line_height: Pixels,
10557 pub lines: Vec<HighlightedRangeLine>,
10558 pub color: Hsla,
10559 pub corner_radius: Pixels,
10560}
10561
10562#[derive(Debug)]
10563pub struct HighlightedRangeLine {
10564 pub start_x: Pixels,
10565 pub end_x: Pixels,
10566}
10567
10568impl HighlightedRange {
10569 pub fn paint(&self, fill: bool, bounds: Bounds<Pixels>, window: &mut Window) {
10570 if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
10571 self.paint_lines(self.start_y, &self.lines[0..1], fill, bounds, window);
10572 self.paint_lines(
10573 self.start_y + self.line_height,
10574 &self.lines[1..],
10575 fill,
10576 bounds,
10577 window,
10578 );
10579 } else {
10580 self.paint_lines(self.start_y, &self.lines, fill, bounds, window);
10581 }
10582 }
10583
10584 fn paint_lines(
10585 &self,
10586 start_y: Pixels,
10587 lines: &[HighlightedRangeLine],
10588 fill: bool,
10589 _bounds: Bounds<Pixels>,
10590 window: &mut Window,
10591 ) {
10592 if lines.is_empty() {
10593 return;
10594 }
10595
10596 let first_line = lines.first().unwrap();
10597 let last_line = lines.last().unwrap();
10598
10599 let first_top_left = point(first_line.start_x, start_y);
10600 let first_top_right = point(first_line.end_x, start_y);
10601
10602 let curve_height = point(Pixels::ZERO, self.corner_radius);
10603 let curve_width = |start_x: Pixels, end_x: Pixels| {
10604 let max = (end_x - start_x) / 2.;
10605 let width = if max < self.corner_radius {
10606 max
10607 } else {
10608 self.corner_radius
10609 };
10610
10611 point(width, Pixels::ZERO)
10612 };
10613
10614 let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
10615 let mut builder = if fill {
10616 gpui::PathBuilder::fill()
10617 } else {
10618 gpui::PathBuilder::stroke(px(1.))
10619 };
10620 builder.move_to(first_top_right - top_curve_width);
10621 builder.curve_to(first_top_right + curve_height, first_top_right);
10622
10623 let mut iter = lines.iter().enumerate().peekable();
10624 while let Some((ix, line)) = iter.next() {
10625 let bottom_right = point(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
10626
10627 if let Some((_, next_line)) = iter.peek() {
10628 let next_top_right = point(next_line.end_x, bottom_right.y);
10629
10630 match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() {
10631 Ordering::Equal => {
10632 builder.line_to(bottom_right);
10633 }
10634 Ordering::Less => {
10635 let curve_width = curve_width(next_top_right.x, bottom_right.x);
10636 builder.line_to(bottom_right - curve_height);
10637 if self.corner_radius > Pixels::ZERO {
10638 builder.curve_to(bottom_right - curve_width, bottom_right);
10639 }
10640 builder.line_to(next_top_right + curve_width);
10641 if self.corner_radius > Pixels::ZERO {
10642 builder.curve_to(next_top_right + curve_height, next_top_right);
10643 }
10644 }
10645 Ordering::Greater => {
10646 let curve_width = curve_width(bottom_right.x, next_top_right.x);
10647 builder.line_to(bottom_right - curve_height);
10648 if self.corner_radius > Pixels::ZERO {
10649 builder.curve_to(bottom_right + curve_width, bottom_right);
10650 }
10651 builder.line_to(next_top_right - curve_width);
10652 if self.corner_radius > Pixels::ZERO {
10653 builder.curve_to(next_top_right + curve_height, next_top_right);
10654 }
10655 }
10656 }
10657 } else {
10658 let curve_width = curve_width(line.start_x, line.end_x);
10659 builder.line_to(bottom_right - curve_height);
10660 if self.corner_radius > Pixels::ZERO {
10661 builder.curve_to(bottom_right - curve_width, bottom_right);
10662 }
10663
10664 let bottom_left = point(line.start_x, bottom_right.y);
10665 builder.line_to(bottom_left + curve_width);
10666 if self.corner_radius > Pixels::ZERO {
10667 builder.curve_to(bottom_left - curve_height, bottom_left);
10668 }
10669 }
10670 }
10671
10672 if first_line.start_x > last_line.start_x {
10673 let curve_width = curve_width(last_line.start_x, first_line.start_x);
10674 let second_top_left = point(last_line.start_x, start_y + self.line_height);
10675 builder.line_to(second_top_left + curve_height);
10676 if self.corner_radius > Pixels::ZERO {
10677 builder.curve_to(second_top_left + curve_width, second_top_left);
10678 }
10679 let first_bottom_left = point(first_line.start_x, second_top_left.y);
10680 builder.line_to(first_bottom_left - curve_width);
10681 if self.corner_radius > Pixels::ZERO {
10682 builder.curve_to(first_bottom_left - curve_height, first_bottom_left);
10683 }
10684 }
10685
10686 builder.line_to(first_top_left + curve_height);
10687 if self.corner_radius > Pixels::ZERO {
10688 builder.curve_to(first_top_left + top_curve_width, first_top_left);
10689 }
10690 builder.line_to(first_top_right - top_curve_width);
10691
10692 if let Ok(path) = builder.build() {
10693 window.paint_path(path, self.color);
10694 }
10695 }
10696}
10697
10698enum CursorPopoverType {
10699 CodeContextMenu,
10700 EditPrediction,
10701}
10702
10703pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
10704 (delta.pow(1.2) / 100.0).min(px(3.0)).into()
10705}
10706
10707fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
10708 (delta.pow(1.2) / 300.0).into()
10709}
10710
10711pub fn register_action<T: Action>(
10712 editor: &Entity<Editor>,
10713 window: &mut Window,
10714 listener: impl Fn(&mut Editor, &T, &mut Window, &mut Context<Editor>) + 'static,
10715) {
10716 let editor = editor.clone();
10717 window.on_action(TypeId::of::<T>(), move |action, phase, window, cx| {
10718 let action = action.downcast_ref().unwrap();
10719 if phase == DispatchPhase::Bubble {
10720 editor.update(cx, |editor, cx| {
10721 listener(editor, action, window, cx);
10722 })
10723 }
10724 })
10725}
10726
10727fn compute_auto_height_layout(
10728 editor: &mut Editor,
10729 min_lines: usize,
10730 max_lines: Option<usize>,
10731 max_line_number_width: Pixels,
10732 known_dimensions: Size<Option<Pixels>>,
10733 available_width: AvailableSpace,
10734 window: &mut Window,
10735 cx: &mut Context<Editor>,
10736) -> Option<Size<Pixels>> {
10737 let width = known_dimensions.width.or({
10738 if let AvailableSpace::Definite(available_width) = available_width {
10739 Some(available_width)
10740 } else {
10741 None
10742 }
10743 })?;
10744 if let Some(height) = known_dimensions.height {
10745 return Some(size(width, height));
10746 }
10747
10748 let style = editor.style.as_ref().unwrap();
10749 let font_id = window.text_system().resolve_font(&style.text.font());
10750 let font_size = style.text.font_size.to_pixels(window.rem_size());
10751 let line_height = style.text.line_height_in_pixels(window.rem_size());
10752 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
10753
10754 let mut snapshot = editor.snapshot(window, cx);
10755 let gutter_dimensions = snapshot
10756 .gutter_dimensions(font_id, font_size, max_line_number_width, cx)
10757 .or_else(|| {
10758 editor
10759 .offset_content
10760 .then(|| GutterDimensions::default_with_margin(font_id, font_size, cx))
10761 })
10762 .unwrap_or_default();
10763
10764 editor.gutter_dimensions = gutter_dimensions;
10765 let text_width = width - gutter_dimensions.width;
10766 let overscroll = size(em_width, px(0.));
10767
10768 let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
10769 if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None)
10770 && editor.set_wrap_width(Some(editor_width), cx)
10771 {
10772 snapshot = editor.snapshot(window, cx);
10773 }
10774
10775 let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
10776
10777 let min_height = line_height * min_lines as f32;
10778 let content_height = scroll_height.max(min_height);
10779
10780 let final_height = if let Some(max_lines) = max_lines {
10781 let max_height = line_height * max_lines as f32;
10782 content_height.min(max_height)
10783 } else {
10784 content_height
10785 };
10786
10787 Some(size(width, final_height))
10788}
10789
10790#[cfg(test)]
10791mod tests {
10792 use super::*;
10793 use crate::{
10794 Editor, MultiBuffer, SelectionEffects,
10795 display_map::{BlockPlacement, BlockProperties},
10796 editor_tests::{init_test, update_test_language_settings},
10797 };
10798 use gpui::{TestAppContext, VisualTestContext};
10799 use language::language_settings;
10800 use log::info;
10801 use std::num::NonZeroU32;
10802 use util::test::sample_text;
10803
10804 #[gpui::test]
10805 async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) {
10806 init_test(cx, |_| {});
10807
10808 let window = cx.add_window(|window, cx| {
10809 let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
10810 let mut editor = Editor::new(
10811 EditorMode::AutoHeight {
10812 min_lines: 1,
10813 max_lines: None,
10814 },
10815 buffer,
10816 None,
10817 window,
10818 cx,
10819 );
10820 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
10821 editor
10822 });
10823 let cx = &mut VisualTestContext::from_window(*window, cx);
10824 let editor = window.root(cx).unwrap();
10825 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
10826
10827 for x in 1..=100 {
10828 let (_, state) = cx.draw(
10829 Default::default(),
10830 size(px(200. + 0.13 * x as f32), px(500.)),
10831 |_, _| EditorElement::new(&editor, style.clone()),
10832 );
10833
10834 assert!(
10835 state.position_map.scroll_max.x == 0.,
10836 "Soft wrapped editor should have no horizontal scrolling!"
10837 );
10838 }
10839 }
10840
10841 #[gpui::test]
10842 async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) {
10843 init_test(cx, |_| {});
10844
10845 let window = cx.add_window(|window, cx| {
10846 let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
10847 let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
10848 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
10849 editor
10850 });
10851 let cx = &mut VisualTestContext::from_window(*window, cx);
10852 let editor = window.root(cx).unwrap();
10853 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
10854
10855 for x in 1..=100 {
10856 let (_, state) = cx.draw(
10857 Default::default(),
10858 size(px(200. + 0.13 * x as f32), px(500.)),
10859 |_, _| EditorElement::new(&editor, style.clone()),
10860 );
10861
10862 assert!(
10863 state.position_map.scroll_max.x == 0.,
10864 "Soft wrapped editor should have no horizontal scrolling!"
10865 );
10866 }
10867 }
10868
10869 #[gpui::test]
10870 fn test_shape_line_numbers(cx: &mut TestAppContext) {
10871 init_test(cx, |_| {});
10872 let window = cx.add_window(|window, cx| {
10873 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
10874 Editor::new(EditorMode::full(), buffer, None, window, cx)
10875 });
10876
10877 let editor = window.root(cx).unwrap();
10878 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
10879 let line_height = window
10880 .update(cx, |_, window, _| {
10881 style.text.line_height_in_pixels(window.rem_size())
10882 })
10883 .unwrap();
10884 let element = EditorElement::new(&editor, style);
10885 let snapshot = window
10886 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
10887 .unwrap();
10888
10889 let layouts = cx
10890 .update_window(*window, |_, window, cx| {
10891 element.layout_line_numbers(
10892 None,
10893 GutterDimensions {
10894 left_padding: Pixels::ZERO,
10895 right_padding: Pixels::ZERO,
10896 width: px(30.0),
10897 margin: Pixels::ZERO,
10898 git_blame_entries_width: None,
10899 },
10900 line_height,
10901 gpui::Point::default(),
10902 DisplayRow(0)..DisplayRow(6),
10903 &(0..6)
10904 .map(|row| RowInfo {
10905 buffer_row: Some(row),
10906 ..Default::default()
10907 })
10908 .collect::<Vec<_>>(),
10909 &BTreeMap::default(),
10910 Some(DisplayPoint::new(DisplayRow(0), 0)),
10911 &snapshot,
10912 window,
10913 cx,
10914 )
10915 })
10916 .unwrap();
10917 assert_eq!(layouts.len(), 6);
10918
10919 let relative_rows = window
10920 .update(cx, |editor, window, cx| {
10921 let snapshot = editor.snapshot(window, cx);
10922 element.calculate_relative_line_numbers(
10923 &snapshot,
10924 &(DisplayRow(0)..DisplayRow(6)),
10925 Some(DisplayRow(3)),
10926 false,
10927 )
10928 })
10929 .unwrap();
10930 assert_eq!(relative_rows[&DisplayRow(0)], 3);
10931 assert_eq!(relative_rows[&DisplayRow(1)], 2);
10932 assert_eq!(relative_rows[&DisplayRow(2)], 1);
10933 // current line has no relative number
10934 assert_eq!(relative_rows[&DisplayRow(4)], 1);
10935 assert_eq!(relative_rows[&DisplayRow(5)], 2);
10936
10937 // works if cursor is before screen
10938 let relative_rows = window
10939 .update(cx, |editor, window, cx| {
10940 let snapshot = editor.snapshot(window, cx);
10941 element.calculate_relative_line_numbers(
10942 &snapshot,
10943 &(DisplayRow(3)..DisplayRow(6)),
10944 Some(DisplayRow(1)),
10945 false,
10946 )
10947 })
10948 .unwrap();
10949 assert_eq!(relative_rows.len(), 3);
10950 assert_eq!(relative_rows[&DisplayRow(3)], 2);
10951 assert_eq!(relative_rows[&DisplayRow(4)], 3);
10952 assert_eq!(relative_rows[&DisplayRow(5)], 4);
10953
10954 // works if cursor is after screen
10955 let relative_rows = window
10956 .update(cx, |editor, window, cx| {
10957 let snapshot = editor.snapshot(window, cx);
10958 element.calculate_relative_line_numbers(
10959 &snapshot,
10960 &(DisplayRow(0)..DisplayRow(3)),
10961 Some(DisplayRow(6)),
10962 false,
10963 )
10964 })
10965 .unwrap();
10966 assert_eq!(relative_rows.len(), 3);
10967 assert_eq!(relative_rows[&DisplayRow(0)], 5);
10968 assert_eq!(relative_rows[&DisplayRow(1)], 4);
10969 assert_eq!(relative_rows[&DisplayRow(2)], 3);
10970 }
10971
10972 #[gpui::test]
10973 fn test_shape_line_numbers_wrapping(cx: &mut TestAppContext) {
10974 init_test(cx, |_| {});
10975 let window = cx.add_window(|window, cx| {
10976 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
10977 Editor::new(EditorMode::full(), buffer, None, window, cx)
10978 });
10979
10980 update_test_language_settings(cx, |s| {
10981 s.defaults.preferred_line_length = Some(5_u32);
10982 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
10983 });
10984
10985 let editor = window.root(cx).unwrap();
10986 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
10987 let line_height = window
10988 .update(cx, |_, window, _| {
10989 style.text.line_height_in_pixels(window.rem_size())
10990 })
10991 .unwrap();
10992 let element = EditorElement::new(&editor, style);
10993 let snapshot = window
10994 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
10995 .unwrap();
10996
10997 let layouts = cx
10998 .update_window(*window, |_, window, cx| {
10999 element.layout_line_numbers(
11000 None,
11001 GutterDimensions {
11002 left_padding: Pixels::ZERO,
11003 right_padding: Pixels::ZERO,
11004 width: px(30.0),
11005 margin: Pixels::ZERO,
11006 git_blame_entries_width: None,
11007 },
11008 line_height,
11009 gpui::Point::default(),
11010 DisplayRow(0)..DisplayRow(6),
11011 &(0..6)
11012 .map(|row| RowInfo {
11013 buffer_row: Some(row),
11014 ..Default::default()
11015 })
11016 .collect::<Vec<_>>(),
11017 &BTreeMap::default(),
11018 Some(DisplayPoint::new(DisplayRow(0), 0)),
11019 &snapshot,
11020 window,
11021 cx,
11022 )
11023 })
11024 .unwrap();
11025 assert_eq!(layouts.len(), 3);
11026
11027 let relative_rows = window
11028 .update(cx, |editor, window, cx| {
11029 let snapshot = editor.snapshot(window, cx);
11030 element.calculate_relative_line_numbers(
11031 &snapshot,
11032 &(DisplayRow(0)..DisplayRow(6)),
11033 Some(DisplayRow(3)),
11034 true,
11035 )
11036 })
11037 .unwrap();
11038
11039 assert_eq!(relative_rows[&DisplayRow(0)], 3);
11040 assert_eq!(relative_rows[&DisplayRow(1)], 2);
11041 assert_eq!(relative_rows[&DisplayRow(2)], 1);
11042 // current line has no relative number
11043 assert_eq!(relative_rows[&DisplayRow(4)], 1);
11044 assert_eq!(relative_rows[&DisplayRow(5)], 2);
11045 }
11046
11047 #[gpui::test]
11048 async fn test_vim_visual_selections(cx: &mut TestAppContext) {
11049 init_test(cx, |_| {});
11050
11051 let window = cx.add_window(|window, cx| {
11052 let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
11053 Editor::new(EditorMode::full(), buffer, None, window, cx)
11054 });
11055 let cx = &mut VisualTestContext::from_window(*window, cx);
11056 let editor = window.root(cx).unwrap();
11057 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
11058
11059 window
11060 .update(cx, |editor, window, cx| {
11061 editor.cursor_shape = CursorShape::Block;
11062 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11063 s.select_ranges([
11064 Point::new(0, 0)..Point::new(1, 0),
11065 Point::new(3, 2)..Point::new(3, 3),
11066 Point::new(5, 6)..Point::new(6, 0),
11067 ]);
11068 });
11069 })
11070 .unwrap();
11071
11072 let (_, state) = cx.draw(
11073 point(px(500.), px(500.)),
11074 size(px(500.), px(500.)),
11075 |_, _| EditorElement::new(&editor, style),
11076 );
11077
11078 assert_eq!(state.selections.len(), 1);
11079 let local_selections = &state.selections[0].1;
11080 assert_eq!(local_selections.len(), 3);
11081 // moves cursor back one line
11082 assert_eq!(
11083 local_selections[0].head,
11084 DisplayPoint::new(DisplayRow(0), 6)
11085 );
11086 assert_eq!(
11087 local_selections[0].range,
11088 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0)
11089 );
11090
11091 // moves cursor back one column
11092 assert_eq!(
11093 local_selections[1].range,
11094 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 3)
11095 );
11096 assert_eq!(
11097 local_selections[1].head,
11098 DisplayPoint::new(DisplayRow(3), 2)
11099 );
11100
11101 // leaves cursor on the max point
11102 assert_eq!(
11103 local_selections[2].range,
11104 DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(6), 0)
11105 );
11106 assert_eq!(
11107 local_selections[2].head,
11108 DisplayPoint::new(DisplayRow(6), 0)
11109 );
11110
11111 // active lines does not include 1 (even though the range of the selection does)
11112 assert_eq!(
11113 state.active_rows.keys().cloned().collect::<Vec<_>>(),
11114 vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
11115 );
11116 }
11117
11118 #[gpui::test]
11119 fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
11120 init_test(cx, |_| {});
11121
11122 let window = cx.add_window(|window, cx| {
11123 let buffer = MultiBuffer::build_simple("", cx);
11124 Editor::new(EditorMode::full(), buffer, None, window, cx)
11125 });
11126 let cx = &mut VisualTestContext::from_window(*window, cx);
11127 let editor = window.root(cx).unwrap();
11128 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
11129 window
11130 .update(cx, |editor, window, cx| {
11131 editor.set_placeholder_text("hello", window, cx);
11132 editor.insert_blocks(
11133 [BlockProperties {
11134 style: BlockStyle::Fixed,
11135 placement: BlockPlacement::Above(Anchor::min()),
11136 height: Some(3),
11137 render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
11138 priority: 0,
11139 }],
11140 None,
11141 cx,
11142 );
11143
11144 // Blur the editor so that it displays placeholder text.
11145 window.blur();
11146 })
11147 .unwrap();
11148
11149 let (_, state) = cx.draw(
11150 point(px(500.), px(500.)),
11151 size(px(500.), px(500.)),
11152 |_, _| EditorElement::new(&editor, style),
11153 );
11154 assert_eq!(state.position_map.line_layouts.len(), 4);
11155 assert_eq!(state.line_numbers.len(), 1);
11156 assert_eq!(
11157 state
11158 .line_numbers
11159 .get(&MultiBufferRow(0))
11160 .map(|line_number| line_number
11161 .segments
11162 .first()
11163 .unwrap()
11164 .shaped_line
11165 .text
11166 .as_ref()),
11167 Some("1")
11168 );
11169 }
11170
11171 #[gpui::test]
11172 fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
11173 const TAB_SIZE: u32 = 4;
11174
11175 let input_text = "\t \t|\t| a b";
11176 let expected_invisibles = vec![
11177 Invisible::Tab {
11178 line_start_offset: 0,
11179 line_end_offset: TAB_SIZE as usize,
11180 },
11181 Invisible::Whitespace {
11182 line_offset: TAB_SIZE as usize,
11183 },
11184 Invisible::Tab {
11185 line_start_offset: TAB_SIZE as usize + 1,
11186 line_end_offset: TAB_SIZE as usize * 2,
11187 },
11188 Invisible::Tab {
11189 line_start_offset: TAB_SIZE as usize * 2 + 1,
11190 line_end_offset: TAB_SIZE as usize * 3,
11191 },
11192 Invisible::Whitespace {
11193 line_offset: TAB_SIZE as usize * 3 + 1,
11194 },
11195 Invisible::Whitespace {
11196 line_offset: TAB_SIZE as usize * 3 + 3,
11197 },
11198 ];
11199 assert_eq!(
11200 expected_invisibles.len(),
11201 input_text
11202 .chars()
11203 .filter(|initial_char| initial_char.is_whitespace())
11204 .count(),
11205 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
11206 );
11207
11208 for show_line_numbers in [true, false] {
11209 init_test(cx, |s| {
11210 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
11211 s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
11212 });
11213
11214 let actual_invisibles = collect_invisibles_from_new_editor(
11215 cx,
11216 EditorMode::full(),
11217 input_text,
11218 px(500.0),
11219 show_line_numbers,
11220 );
11221
11222 assert_eq!(expected_invisibles, actual_invisibles);
11223 }
11224 }
11225
11226 #[gpui::test]
11227 fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
11228 init_test(cx, |s| {
11229 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
11230 s.defaults.tab_size = NonZeroU32::new(4);
11231 });
11232
11233 for editor_mode_without_invisibles in [
11234 EditorMode::SingleLine,
11235 EditorMode::AutoHeight {
11236 min_lines: 1,
11237 max_lines: Some(100),
11238 },
11239 ] {
11240 for show_line_numbers in [true, false] {
11241 let invisibles = collect_invisibles_from_new_editor(
11242 cx,
11243 editor_mode_without_invisibles.clone(),
11244 "\t\t\t| | a b",
11245 px(500.0),
11246 show_line_numbers,
11247 );
11248 assert!(
11249 invisibles.is_empty(),
11250 "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"
11251 );
11252 }
11253 }
11254 }
11255
11256 #[gpui::test]
11257 fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
11258 let tab_size = 4;
11259 let input_text = "a\tbcd ".repeat(9);
11260 let repeated_invisibles = [
11261 Invisible::Tab {
11262 line_start_offset: 1,
11263 line_end_offset: tab_size as usize,
11264 },
11265 Invisible::Whitespace {
11266 line_offset: tab_size as usize + 3,
11267 },
11268 Invisible::Whitespace {
11269 line_offset: tab_size as usize + 4,
11270 },
11271 Invisible::Whitespace {
11272 line_offset: tab_size as usize + 5,
11273 },
11274 Invisible::Whitespace {
11275 line_offset: tab_size as usize + 6,
11276 },
11277 Invisible::Whitespace {
11278 line_offset: tab_size as usize + 7,
11279 },
11280 ];
11281 let expected_invisibles = std::iter::once(repeated_invisibles)
11282 .cycle()
11283 .take(9)
11284 .flatten()
11285 .collect::<Vec<_>>();
11286 assert_eq!(
11287 expected_invisibles.len(),
11288 input_text
11289 .chars()
11290 .filter(|initial_char| initial_char.is_whitespace())
11291 .count(),
11292 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
11293 );
11294 info!("Expected invisibles: {expected_invisibles:?}");
11295
11296 init_test(cx, |_| {});
11297
11298 // Put the same string with repeating whitespace pattern into editors of various size,
11299 // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
11300 let resize_step = 10.0;
11301 let mut editor_width = 200.0;
11302 while editor_width <= 1000.0 {
11303 for show_line_numbers in [true, false] {
11304 update_test_language_settings(cx, |s| {
11305 s.defaults.tab_size = NonZeroU32::new(tab_size);
11306 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
11307 s.defaults.preferred_line_length = Some(editor_width as u32);
11308 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
11309 });
11310
11311 let actual_invisibles = collect_invisibles_from_new_editor(
11312 cx,
11313 EditorMode::full(),
11314 &input_text,
11315 px(editor_width),
11316 show_line_numbers,
11317 );
11318
11319 // Whatever the editor size is, ensure it has the same invisible kinds in the same order
11320 // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
11321 let mut i = 0;
11322 for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
11323 i = actual_index;
11324 match expected_invisibles.get(i) {
11325 Some(expected_invisible) => match (expected_invisible, actual_invisible) {
11326 (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
11327 | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
11328 _ => {
11329 panic!(
11330 "At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}"
11331 )
11332 }
11333 },
11334 None => {
11335 panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
11336 }
11337 }
11338 }
11339 let missing_expected_invisibles = &expected_invisibles[i + 1..];
11340 assert!(
11341 missing_expected_invisibles.is_empty(),
11342 "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
11343 );
11344
11345 editor_width += resize_step;
11346 }
11347 }
11348 }
11349
11350 fn collect_invisibles_from_new_editor(
11351 cx: &mut TestAppContext,
11352 editor_mode: EditorMode,
11353 input_text: &str,
11354 editor_width: Pixels,
11355 show_line_numbers: bool,
11356 ) -> Vec<Invisible> {
11357 info!(
11358 "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
11359 f32::from(editor_width)
11360 );
11361 let window = cx.add_window(|window, cx| {
11362 let buffer = MultiBuffer::build_simple(input_text, cx);
11363 Editor::new(editor_mode, buffer, None, window, cx)
11364 });
11365 let cx = &mut VisualTestContext::from_window(*window, cx);
11366 let editor = window.root(cx).unwrap();
11367
11368 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
11369 window
11370 .update(cx, |editor, _, cx| {
11371 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
11372 editor.set_wrap_width(Some(editor_width), cx);
11373 editor.set_show_line_numbers(show_line_numbers, cx);
11374 })
11375 .unwrap();
11376 let (_, state) = cx.draw(
11377 point(px(500.), px(500.)),
11378 size(px(500.), px(500.)),
11379 |_, _| EditorElement::new(&editor, style),
11380 );
11381 state
11382 .position_map
11383 .line_layouts
11384 .iter()
11385 .flat_map(|line_with_invisibles| &line_with_invisibles.invisibles)
11386 .cloned()
11387 .collect()
11388 }
11389
11390 #[gpui::test]
11391 fn test_merge_overlapping_ranges() {
11392 let base_bg = Hsla::white();
11393 let color1 = Hsla {
11394 h: 0.0,
11395 s: 0.5,
11396 l: 0.5,
11397 a: 0.5,
11398 };
11399 let color2 = Hsla {
11400 h: 120.0,
11401 s: 0.5,
11402 l: 0.5,
11403 a: 0.5,
11404 };
11405
11406 let display_point = |col| DisplayPoint::new(DisplayRow(0), col);
11407 let cols = |v: &Vec<(Range<DisplayPoint>, Hsla)>| -> Vec<(u32, u32)> {
11408 v.iter()
11409 .map(|(r, _)| (r.start.column(), r.end.column()))
11410 .collect()
11411 };
11412
11413 // Test overlapping ranges blend colors
11414 let overlapping = vec![
11415 (display_point(5)..display_point(15), color1),
11416 (display_point(10)..display_point(20), color2),
11417 ];
11418 let result = EditorElement::merge_overlapping_ranges(overlapping, base_bg);
11419 assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
11420
11421 // Test middle segment should have blended color
11422 let blended = Hsla::blend(Hsla::blend(base_bg, color1), color2);
11423 assert_eq!(result[1].1, blended);
11424
11425 // Test adjacent same-color ranges merge
11426 let adjacent_same = vec![
11427 (display_point(5)..display_point(10), color1),
11428 (display_point(10)..display_point(15), color1),
11429 ];
11430 let result = EditorElement::merge_overlapping_ranges(adjacent_same, base_bg);
11431 assert_eq!(cols(&result), vec![(5, 15)]);
11432
11433 // Test contained range splits
11434 let contained = vec![
11435 (display_point(5)..display_point(20), color1),
11436 (display_point(10)..display_point(15), color2),
11437 ];
11438 let result = EditorElement::merge_overlapping_ranges(contained, base_bg);
11439 assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
11440
11441 // Test multiple overlaps split at every boundary
11442 let color3 = Hsla {
11443 h: 240.0,
11444 s: 0.5,
11445 l: 0.5,
11446 a: 0.5,
11447 };
11448 let complex = vec![
11449 (display_point(5)..display_point(12), color1),
11450 (display_point(8)..display_point(16), color2),
11451 (display_point(10)..display_point(14), color3),
11452 ];
11453 let result = EditorElement::merge_overlapping_ranges(complex, base_bg);
11454 assert_eq!(
11455 cols(&result),
11456 vec![(5, 8), (8, 10), (10, 12), (12, 14), (14, 16)]
11457 );
11458 }
11459
11460 #[gpui::test]
11461 fn test_bg_segments_per_row() {
11462 let base_bg = Hsla::white();
11463
11464 // Case A: selection spans three display rows: row 1 [5, end), full row 2, row 3 [0, 7)
11465 {
11466 let selection_color = Hsla {
11467 h: 200.0,
11468 s: 0.5,
11469 l: 0.5,
11470 a: 0.5,
11471 };
11472 let player_color = PlayerColor {
11473 cursor: selection_color,
11474 background: selection_color,
11475 selection: selection_color,
11476 };
11477
11478 let spanning_selection = SelectionLayout {
11479 head: DisplayPoint::new(DisplayRow(3), 7),
11480 cursor_shape: CursorShape::Bar,
11481 is_newest: true,
11482 is_local: true,
11483 range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 7),
11484 active_rows: DisplayRow(1)..DisplayRow(4),
11485 user_name: None,
11486 };
11487
11488 let selections = vec![(player_color, vec![spanning_selection])];
11489 let result = EditorElement::bg_segments_per_row(
11490 DisplayRow(0)..DisplayRow(5),
11491 &selections,
11492 &[],
11493 base_bg,
11494 );
11495
11496 assert_eq!(result.len(), 5);
11497 assert!(result[0].is_empty());
11498 assert_eq!(result[1].len(), 1);
11499 assert_eq!(result[2].len(), 1);
11500 assert_eq!(result[3].len(), 1);
11501 assert!(result[4].is_empty());
11502
11503 assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
11504 assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
11505 assert_eq!(result[1][0].0.end.column(), u32::MAX);
11506 assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
11507 assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
11508 assert_eq!(result[2][0].0.end.column(), u32::MAX);
11509 assert_eq!(result[3][0].0.start, DisplayPoint::new(DisplayRow(3), 0));
11510 assert_eq!(result[3][0].0.end, DisplayPoint::new(DisplayRow(3), 7));
11511 }
11512
11513 // Case B: selection ends exactly at the start of row 3, excluding row 3
11514 {
11515 let selection_color = Hsla {
11516 h: 120.0,
11517 s: 0.5,
11518 l: 0.5,
11519 a: 0.5,
11520 };
11521 let player_color = PlayerColor {
11522 cursor: selection_color,
11523 background: selection_color,
11524 selection: selection_color,
11525 };
11526
11527 let selection = SelectionLayout {
11528 head: DisplayPoint::new(DisplayRow(2), 0),
11529 cursor_shape: CursorShape::Bar,
11530 is_newest: true,
11531 is_local: true,
11532 range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 0),
11533 active_rows: DisplayRow(1)..DisplayRow(3),
11534 user_name: None,
11535 };
11536
11537 let selections = vec![(player_color, vec![selection])];
11538 let result = EditorElement::bg_segments_per_row(
11539 DisplayRow(0)..DisplayRow(4),
11540 &selections,
11541 &[],
11542 base_bg,
11543 );
11544
11545 assert_eq!(result.len(), 4);
11546 assert!(result[0].is_empty());
11547 assert_eq!(result[1].len(), 1);
11548 assert_eq!(result[2].len(), 1);
11549 assert!(result[3].is_empty());
11550
11551 assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
11552 assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
11553 assert_eq!(result[1][0].0.end.column(), u32::MAX);
11554 assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
11555 assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
11556 assert_eq!(result[2][0].0.end.column(), u32::MAX);
11557 }
11558 }
11559
11560 #[cfg(test)]
11561 fn generate_test_run(len: usize, color: Hsla) -> TextRun {
11562 TextRun {
11563 len,
11564 color,
11565 ..Default::default()
11566 }
11567 }
11568
11569 #[gpui::test]
11570 fn test_split_runs_by_bg_segments(cx: &mut gpui::TestAppContext) {
11571 init_test(cx, |_| {});
11572
11573 let dx = |start: u32, end: u32| {
11574 DisplayPoint::new(DisplayRow(0), start)..DisplayPoint::new(DisplayRow(0), end)
11575 };
11576
11577 let text_color = Hsla {
11578 h: 210.0,
11579 s: 0.1,
11580 l: 0.4,
11581 a: 1.0,
11582 };
11583 let bg_1 = Hsla {
11584 h: 30.0,
11585 s: 0.6,
11586 l: 0.8,
11587 a: 1.0,
11588 };
11589 let bg_2 = Hsla {
11590 h: 200.0,
11591 s: 0.6,
11592 l: 0.2,
11593 a: 1.0,
11594 };
11595 let min_contrast = 45.0;
11596 let adjusted_bg1 = ensure_minimum_contrast(text_color, bg_1, min_contrast);
11597 let adjusted_bg2 = ensure_minimum_contrast(text_color, bg_2, min_contrast);
11598
11599 // Case A: single run; disjoint segments inside the run
11600 {
11601 let runs = vec![generate_test_run(20, text_color)];
11602 let segs = vec![(dx(5, 10), bg_1), (dx(12, 16), bg_2)];
11603 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
11604 // Expected slices: [0,5) [5,10) [10,12) [12,16) [16,20)
11605 assert_eq!(
11606 out.iter().map(|r| r.len).collect::<Vec<_>>(),
11607 vec![5, 5, 2, 4, 4]
11608 );
11609 assert_eq!(out[0].color, text_color);
11610 assert_eq!(out[1].color, adjusted_bg1);
11611 assert_eq!(out[2].color, text_color);
11612 assert_eq!(out[3].color, adjusted_bg2);
11613 assert_eq!(out[4].color, text_color);
11614 }
11615
11616 // Case B: multiple runs; segment extends to end of line (u32::MAX)
11617 {
11618 let runs = vec![
11619 generate_test_run(8, text_color),
11620 generate_test_run(7, text_color),
11621 ];
11622 let segs = vec![(dx(6, u32::MAX), bg_1)];
11623 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
11624 // Expected slices across runs: [0,6) [6,8) | [0,7)
11625 assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![6, 2, 7]);
11626 assert_eq!(out[0].color, text_color);
11627 assert_eq!(out[1].color, adjusted_bg1);
11628 assert_eq!(out[2].color, adjusted_bg1);
11629 }
11630
11631 // Case C: multi-byte characters
11632 {
11633 // for text: "Hello π δΈη!"
11634 let runs = vec![
11635 generate_test_run(5, text_color), // "Hello"
11636 generate_test_run(6, text_color), // " π "
11637 generate_test_run(6, text_color), // "δΈη"
11638 generate_test_run(1, text_color), // "!"
11639 ];
11640 // selecting "π δΈ"
11641 let segs = vec![(dx(6, 14), bg_1)];
11642 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
11643 // "Hello" | " " | "π " | "δΈ" | "η" | "!"
11644 assert_eq!(
11645 out.iter().map(|r| r.len).collect::<Vec<_>>(),
11646 vec![5, 1, 5, 3, 3, 1]
11647 );
11648 assert_eq!(out[0].color, text_color); // "Hello"
11649 assert_eq!(out[2].color, adjusted_bg1); // "π "
11650 assert_eq!(out[3].color, adjusted_bg1); // "δΈ"
11651 assert_eq!(out[4].color, text_color); // "η"
11652 assert_eq!(out[5].color, text_color); // "!"
11653 }
11654
11655 // Case D: split multiple consecutive text runs with segments
11656 {
11657 let segs = vec![
11658 (dx(2, 4), bg_1), // selecting "cd"
11659 (dx(4, 8), bg_2), // selecting "efgh"
11660 (dx(9, 11), bg_1), // selecting "jk"
11661 (dx(12, 16), bg_2), // selecting "mnop"
11662 (dx(18, 19), bg_1), // selecting "s"
11663 ];
11664
11665 // for text: "abcdef"
11666 let runs = vec![
11667 generate_test_run(2, text_color), // ab
11668 generate_test_run(4, text_color), // cdef
11669 ];
11670 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
11671 // new splits "ab", "cd", "ef"
11672 assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![2, 2, 2]);
11673 assert_eq!(out[0].color, text_color);
11674 assert_eq!(out[1].color, adjusted_bg1);
11675 assert_eq!(out[2].color, adjusted_bg2);
11676
11677 // for text: "ghijklmn"
11678 let runs = vec![
11679 generate_test_run(3, text_color), // ghi
11680 generate_test_run(2, text_color), // jk
11681 generate_test_run(3, text_color), // lmn
11682 ];
11683 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 6); // 2 + 4 from first run
11684 // new splits "gh", "i", "jk", "l", "mn"
11685 assert_eq!(
11686 out.iter().map(|r| r.len).collect::<Vec<_>>(),
11687 vec![2, 1, 2, 1, 2]
11688 );
11689 assert_eq!(out[0].color, adjusted_bg2);
11690 assert_eq!(out[1].color, text_color);
11691 assert_eq!(out[2].color, adjusted_bg1);
11692 assert_eq!(out[3].color, text_color);
11693 assert_eq!(out[4].color, adjusted_bg2);
11694
11695 // for text: "opqrs"
11696 let runs = vec![
11697 generate_test_run(1, text_color), // o
11698 generate_test_run(4, text_color), // pqrs
11699 ];
11700 let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 14); // 6 + 3 + 2 + 3 from first two runs
11701 // new splits "o", "p", "qr", "s"
11702 assert_eq!(
11703 out.iter().map(|r| r.len).collect::<Vec<_>>(),
11704 vec![1, 1, 2, 1]
11705 );
11706 assert_eq!(out[0].color, adjusted_bg2);
11707 assert_eq!(out[1].color, adjusted_bg2);
11708 assert_eq!(out[2].color, text_color);
11709 assert_eq!(out[3].color, adjusted_bg1);
11710 }
11711 }
11712}