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