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