1use crate::{
2 code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
3 commit_tooltip::{blame_entry_relative_timestamp, CommitTooltip, ParsedCommitMessage},
4 display_map::{
5 Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
6 },
7 editor_settings::{
8 CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
9 ScrollbarDiagnostics, ShowScrollbar,
10 },
11 git::blame::GitBlame,
12 hover_popover::{
13 self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
14 },
15 inlay_hint_settings,
16 items::BufferSearchHighlights,
17 mouse_context_menu::{self, MenuPosition, MouseContextMenu},
18 scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
19 BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint,
20 DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
21 EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
22 GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
23 InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
24 OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
25 Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
26 CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
27 MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
28};
29use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
30use client::ParticipantIndex;
31use collections::{BTreeMap, HashMap, HashSet};
32use file_icons::FileIcons;
33use git::{blame::BlameEntry, status::FileStatus, Oid};
34use gpui::{
35 anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
36 relative, size, solid_background, svg, transparent_black, Action, AnyElement, App,
37 AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners,
38 CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _,
39 FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
40 ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
41 ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
42 StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, Window,
43};
44use inline_completion::Direction;
45use itertools::Itertools;
46use language::{
47 language_settings::{
48 IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
49 ShowWhitespaceSetting,
50 },
51 ChunkRendererContext,
52};
53use lsp::DiagnosticSeverity;
54use multi_buffer::{
55 Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
56 RowInfo,
57};
58use project::project_settings::{self, GitGutterSetting, ProjectSettings};
59use settings::Settings;
60use smallvec::{smallvec, SmallVec};
61use std::{
62 any::TypeId,
63 borrow::Cow,
64 cmp::{self, Ordering},
65 fmt::{self, Write},
66 iter, mem,
67 ops::{Deref, Range},
68 rc::Rc,
69 sync::Arc,
70};
71use sum_tree::Bias;
72use text::BufferId;
73use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
74use ui::{
75 h_flex, prelude::*, ButtonLike, ButtonStyle, ContextMenu, IconButtonShape, KeyBinding, Tooltip,
76 POPOVER_Y_PADDING,
77};
78use unicode_segmentation::UnicodeSegmentation;
79use util::{debug_panic, RangeExt, ResultExt};
80use workspace::{item::Item, notifications::NotifyTaskExt};
81
82const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
83const MIN_SCROLL_THUMB_SIZE: f32 = 25.;
84
85struct SelectionLayout {
86 head: DisplayPoint,
87 cursor_shape: CursorShape,
88 is_newest: bool,
89 is_local: bool,
90 range: Range<DisplayPoint>,
91 active_rows: Range<DisplayRow>,
92 user_name: Option<SharedString>,
93}
94
95impl SelectionLayout {
96 fn new<T: ToPoint + ToDisplayPoint + Clone>(
97 selection: Selection<T>,
98 line_mode: bool,
99 cursor_shape: CursorShape,
100 map: &DisplaySnapshot,
101 is_newest: bool,
102 is_local: bool,
103 user_name: Option<SharedString>,
104 ) -> Self {
105 let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
106 let display_selection = point_selection.map(|p| p.to_display_point(map));
107 let mut range = display_selection.range();
108 let mut head = display_selection.head();
109 let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
110 ..map.next_line_boundary(point_selection.end).1.row();
111
112 // vim visual line mode
113 if line_mode {
114 let point_range = map.expand_to_line(point_selection.range());
115 range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
116 }
117
118 // any vim visual mode (including line mode)
119 if (cursor_shape == CursorShape::Block || cursor_shape == CursorShape::Hollow)
120 && !range.is_empty()
121 && !selection.reversed
122 {
123 if head.column() > 0 {
124 head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
125 } else if head.row().0 > 0 && head != map.max_point() {
126 head = map.clip_point(
127 DisplayPoint::new(
128 head.row().previous_row(),
129 map.line_len(head.row().previous_row()),
130 ),
131 Bias::Left,
132 );
133 // updating range.end is a no-op unless you're cursor is
134 // on the newline containing a multi-buffer divider
135 // in which case the clip_point may have moved the head up
136 // an additional row.
137 range.end = DisplayPoint::new(head.row().next_row(), 0);
138 active_rows.end = head.row();
139 }
140 }
141
142 Self {
143 head,
144 cursor_shape,
145 is_newest,
146 is_local,
147 range,
148 active_rows,
149 user_name,
150 }
151 }
152}
153
154pub struct EditorElement {
155 editor: Entity<Editor>,
156 style: EditorStyle,
157}
158
159type DisplayRowDelta = u32;
160
161impl EditorElement {
162 pub(crate) const SCROLLBAR_WIDTH: Pixels = px(15.);
163
164 pub fn new(editor: &Entity<Editor>, style: EditorStyle) -> Self {
165 Self {
166 editor: editor.clone(),
167 style,
168 }
169 }
170
171 fn register_actions(&self, window: &mut Window, cx: &mut App) {
172 let editor = &self.editor;
173 editor.update(cx, |editor, cx| {
174 for action in editor.editor_actions.borrow().values() {
175 (action)(window, cx)
176 }
177 });
178
179 crate::rust_analyzer_ext::apply_related_actions(editor, window, cx);
180 crate::clangd_ext::apply_related_actions(editor, window, cx);
181 register_action(editor, window, Editor::open_context_menu);
182 register_action(editor, window, Editor::move_left);
183 register_action(editor, window, Editor::move_right);
184 register_action(editor, window, Editor::move_down);
185 register_action(editor, window, Editor::move_down_by_lines);
186 register_action(editor, window, Editor::select_down_by_lines);
187 register_action(editor, window, Editor::move_up);
188 register_action(editor, window, Editor::move_up_by_lines);
189 register_action(editor, window, Editor::select_up_by_lines);
190 register_action(editor, window, Editor::select_page_down);
191 register_action(editor, window, Editor::select_page_up);
192 register_action(editor, window, Editor::cancel);
193 register_action(editor, window, Editor::newline);
194 register_action(editor, window, Editor::newline_above);
195 register_action(editor, window, Editor::newline_below);
196 register_action(editor, window, Editor::backspace);
197 register_action(editor, window, Editor::delete);
198 register_action(editor, window, Editor::tab);
199 register_action(editor, window, Editor::backtab);
200 register_action(editor, window, Editor::indent);
201 register_action(editor, window, Editor::outdent);
202 register_action(editor, window, Editor::autoindent);
203 register_action(editor, window, Editor::delete_line);
204 register_action(editor, window, Editor::join_lines);
205 register_action(editor, window, Editor::sort_lines_case_sensitive);
206 register_action(editor, window, Editor::sort_lines_case_insensitive);
207 register_action(editor, window, Editor::reverse_lines);
208 register_action(editor, window, Editor::shuffle_lines);
209 register_action(editor, window, Editor::convert_to_upper_case);
210 register_action(editor, window, Editor::convert_to_lower_case);
211 register_action(editor, window, Editor::convert_to_title_case);
212 register_action(editor, window, Editor::convert_to_snake_case);
213 register_action(editor, window, Editor::convert_to_kebab_case);
214 register_action(editor, window, Editor::convert_to_upper_camel_case);
215 register_action(editor, window, Editor::convert_to_lower_camel_case);
216 register_action(editor, window, Editor::convert_to_opposite_case);
217 register_action(editor, window, Editor::delete_to_previous_word_start);
218 register_action(editor, window, Editor::delete_to_previous_subword_start);
219 register_action(editor, window, Editor::delete_to_next_word_end);
220 register_action(editor, window, Editor::delete_to_next_subword_end);
221 register_action(editor, window, Editor::delete_to_beginning_of_line);
222 register_action(editor, window, Editor::delete_to_end_of_line);
223 register_action(editor, window, Editor::cut_to_end_of_line);
224 register_action(editor, window, Editor::duplicate_line_up);
225 register_action(editor, window, Editor::duplicate_line_down);
226 register_action(editor, window, Editor::duplicate_selection);
227 register_action(editor, window, Editor::move_line_up);
228 register_action(editor, window, Editor::move_line_down);
229 register_action(editor, window, Editor::transpose);
230 register_action(editor, window, Editor::rewrap);
231 register_action(editor, window, Editor::cut);
232 register_action(editor, window, Editor::kill_ring_cut);
233 register_action(editor, window, Editor::kill_ring_yank);
234 register_action(editor, window, Editor::copy);
235 register_action(editor, window, Editor::paste);
236 register_action(editor, window, Editor::undo);
237 register_action(editor, window, Editor::redo);
238 register_action(editor, window, Editor::move_page_up);
239 register_action(editor, window, Editor::move_page_down);
240 register_action(editor, window, Editor::next_screen);
241 register_action(editor, window, Editor::scroll_cursor_top);
242 register_action(editor, window, Editor::scroll_cursor_center);
243 register_action(editor, window, Editor::scroll_cursor_bottom);
244 register_action(editor, window, Editor::scroll_cursor_center_top_bottom);
245 register_action(editor, window, |editor, _: &LineDown, window, cx| {
246 editor.scroll_screen(&ScrollAmount::Line(1.), window, cx)
247 });
248 register_action(editor, window, |editor, _: &LineUp, window, cx| {
249 editor.scroll_screen(&ScrollAmount::Line(-1.), window, cx)
250 });
251 register_action(editor, window, |editor, _: &HalfPageDown, window, cx| {
252 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx)
253 });
254 register_action(
255 editor,
256 window,
257 |editor, HandleInput(text): &HandleInput, window, cx| {
258 if text.is_empty() {
259 return;
260 }
261 editor.handle_input(text, window, cx);
262 },
263 );
264 register_action(editor, window, |editor, _: &HalfPageUp, window, cx| {
265 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx)
266 });
267 register_action(editor, window, |editor, _: &PageDown, window, cx| {
268 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx)
269 });
270 register_action(editor, window, |editor, _: &PageUp, window, cx| {
271 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx)
272 });
273 register_action(editor, window, Editor::move_to_previous_word_start);
274 register_action(editor, window, Editor::move_to_previous_subword_start);
275 register_action(editor, window, Editor::move_to_next_word_end);
276 register_action(editor, window, Editor::move_to_next_subword_end);
277 register_action(editor, window, Editor::move_to_beginning_of_line);
278 register_action(editor, window, Editor::move_to_end_of_line);
279 register_action(editor, window, Editor::move_to_start_of_paragraph);
280 register_action(editor, window, Editor::move_to_end_of_paragraph);
281 register_action(editor, window, Editor::move_to_beginning);
282 register_action(editor, window, Editor::move_to_end);
283 register_action(editor, window, Editor::move_to_start_of_excerpt);
284 register_action(editor, window, Editor::move_to_start_of_next_excerpt);
285 register_action(editor, window, Editor::move_to_end_of_excerpt);
286 register_action(editor, window, Editor::move_to_end_of_previous_excerpt);
287 register_action(editor, window, Editor::select_up);
288 register_action(editor, window, Editor::select_down);
289 register_action(editor, window, Editor::select_left);
290 register_action(editor, window, Editor::select_right);
291 register_action(editor, window, Editor::select_to_previous_word_start);
292 register_action(editor, window, Editor::select_to_previous_subword_start);
293 register_action(editor, window, Editor::select_to_next_word_end);
294 register_action(editor, window, Editor::select_to_next_subword_end);
295 register_action(editor, window, Editor::select_to_beginning_of_line);
296 register_action(editor, window, Editor::select_to_end_of_line);
297 register_action(editor, window, Editor::select_to_start_of_paragraph);
298 register_action(editor, window, Editor::select_to_end_of_paragraph);
299 register_action(editor, window, Editor::select_to_start_of_excerpt);
300 register_action(editor, window, Editor::select_to_start_of_next_excerpt);
301 register_action(editor, window, Editor::select_to_end_of_excerpt);
302 register_action(editor, window, Editor::select_to_end_of_previous_excerpt);
303 register_action(editor, window, Editor::select_to_beginning);
304 register_action(editor, window, Editor::select_to_end);
305 register_action(editor, window, Editor::select_all);
306 register_action(editor, window, |editor, action, window, cx| {
307 editor.select_all_matches(action, window, cx).log_err();
308 });
309 register_action(editor, window, Editor::select_line);
310 register_action(editor, window, Editor::split_selection_into_lines);
311 register_action(editor, window, Editor::add_selection_above);
312 register_action(editor, window, Editor::add_selection_below);
313 register_action(editor, window, |editor, action, window, cx| {
314 editor.select_next(action, window, cx).log_err();
315 });
316 register_action(editor, window, |editor, action, window, cx| {
317 editor.select_previous(action, window, cx).log_err();
318 });
319 register_action(editor, window, Editor::toggle_comments);
320 register_action(editor, window, Editor::select_larger_syntax_node);
321 register_action(editor, window, Editor::select_smaller_syntax_node);
322 register_action(editor, window, Editor::select_enclosing_symbol);
323 register_action(editor, window, Editor::move_to_enclosing_bracket);
324 register_action(editor, window, Editor::undo_selection);
325 register_action(editor, window, Editor::redo_selection);
326 if !editor.read(cx).is_singleton(cx) {
327 register_action(editor, window, Editor::expand_excerpts);
328 register_action(editor, window, Editor::expand_excerpts_up);
329 register_action(editor, window, Editor::expand_excerpts_down);
330 }
331 register_action(editor, window, Editor::go_to_diagnostic);
332 register_action(editor, window, Editor::go_to_prev_diagnostic);
333 register_action(editor, window, Editor::go_to_next_hunk);
334 register_action(editor, window, Editor::go_to_prev_hunk);
335 register_action(editor, window, |editor, action, window, cx| {
336 editor
337 .go_to_definition(action, window, cx)
338 .detach_and_log_err(cx);
339 });
340 register_action(editor, window, |editor, action, window, cx| {
341 editor
342 .go_to_definition_split(action, window, cx)
343 .detach_and_log_err(cx);
344 });
345 register_action(editor, window, |editor, action, window, cx| {
346 editor
347 .go_to_declaration(action, window, cx)
348 .detach_and_log_err(cx);
349 });
350 register_action(editor, window, |editor, action, window, cx| {
351 editor
352 .go_to_declaration_split(action, window, cx)
353 .detach_and_log_err(cx);
354 });
355 register_action(editor, window, |editor, action, window, cx| {
356 editor
357 .go_to_implementation(action, window, cx)
358 .detach_and_log_err(cx);
359 });
360 register_action(editor, window, |editor, action, window, cx| {
361 editor
362 .go_to_implementation_split(action, window, cx)
363 .detach_and_log_err(cx);
364 });
365 register_action(editor, window, |editor, action, window, cx| {
366 editor
367 .go_to_type_definition(action, window, cx)
368 .detach_and_log_err(cx);
369 });
370 register_action(editor, window, |editor, action, window, cx| {
371 editor
372 .go_to_type_definition_split(action, window, cx)
373 .detach_and_log_err(cx);
374 });
375 register_action(editor, window, Editor::open_url);
376 register_action(editor, window, Editor::open_selected_filename);
377 register_action(editor, window, Editor::fold);
378 register_action(editor, window, Editor::fold_at_level);
379 register_action(editor, window, Editor::fold_all);
380 register_action(editor, window, Editor::fold_function_bodies);
381 register_action(editor, window, Editor::fold_at);
382 register_action(editor, window, Editor::fold_recursive);
383 register_action(editor, window, Editor::toggle_fold);
384 register_action(editor, window, Editor::toggle_fold_recursive);
385 register_action(editor, window, Editor::unfold_lines);
386 register_action(editor, window, Editor::unfold_recursive);
387 register_action(editor, window, Editor::unfold_all);
388 register_action(editor, window, Editor::unfold_at);
389 register_action(editor, window, Editor::fold_selected_ranges);
390 register_action(editor, window, Editor::set_mark);
391 register_action(editor, window, Editor::swap_selection_ends);
392 register_action(editor, window, Editor::show_completions);
393 register_action(editor, window, Editor::toggle_code_actions);
394 register_action(editor, window, Editor::open_excerpts);
395 register_action(editor, window, Editor::open_excerpts_in_split);
396 register_action(editor, window, Editor::open_proposed_changes_editor);
397 register_action(editor, window, Editor::toggle_soft_wrap);
398 register_action(editor, window, Editor::toggle_tab_bar);
399 register_action(editor, window, Editor::toggle_line_numbers);
400 register_action(editor, window, Editor::toggle_relative_line_numbers);
401 register_action(editor, window, Editor::toggle_indent_guides);
402 register_action(editor, window, Editor::toggle_inlay_hints);
403 register_action(editor, window, Editor::toggle_edit_predictions);
404 register_action(editor, window, Editor::toggle_inline_diagnostics);
405 register_action(editor, window, hover_popover::hover);
406 register_action(editor, window, Editor::reveal_in_finder);
407 register_action(editor, window, Editor::copy_path);
408 register_action(editor, window, Editor::copy_relative_path);
409 register_action(editor, window, Editor::copy_file_name);
410 register_action(editor, window, Editor::copy_file_name_without_extension);
411 register_action(editor, window, Editor::copy_highlight_json);
412 register_action(editor, window, Editor::copy_permalink_to_line);
413 register_action(editor, window, Editor::open_permalink_to_line);
414 register_action(editor, window, Editor::copy_file_location);
415 register_action(editor, window, Editor::toggle_git_blame);
416 register_action(editor, window, Editor::toggle_git_blame_inline);
417 register_action(editor, window, Editor::toggle_selected_diff_hunks);
418 register_action(editor, window, Editor::toggle_staged_selected_diff_hunks);
419 register_action(editor, window, Editor::stage_and_next);
420 register_action(editor, window, Editor::unstage_and_next);
421 register_action(editor, window, Editor::expand_all_diff_hunks);
422
423 register_action(editor, window, |editor, action, window, cx| {
424 if let Some(task) = editor.format(action, window, cx) {
425 task.detach_and_notify_err(window, cx);
426 } else {
427 cx.propagate();
428 }
429 });
430 register_action(editor, window, |editor, action, window, cx| {
431 if let Some(task) = editor.format_selections(action, window, cx) {
432 task.detach_and_notify_err(window, cx);
433 } else {
434 cx.propagate();
435 }
436 });
437 register_action(editor, window, |editor, action, window, cx| {
438 if let Some(task) = editor.organize_imports(action, window, cx) {
439 task.detach_and_notify_err(window, cx);
440 } else {
441 cx.propagate();
442 }
443 });
444 register_action(editor, window, Editor::restart_language_server);
445 register_action(editor, window, Editor::show_character_palette);
446 register_action(editor, window, |editor, action, window, cx| {
447 if let Some(task) = editor.confirm_completion(action, window, cx) {
448 task.detach_and_notify_err(window, cx);
449 } else {
450 cx.propagate();
451 }
452 });
453 register_action(editor, window, |editor, action, window, cx| {
454 if let Some(task) = editor.compose_completion(action, window, cx) {
455 task.detach_and_notify_err(window, cx);
456 } else {
457 cx.propagate();
458 }
459 });
460 register_action(editor, window, |editor, action, window, cx| {
461 if let Some(task) = editor.confirm_code_action(action, window, cx) {
462 task.detach_and_notify_err(window, cx);
463 } else {
464 cx.propagate();
465 }
466 });
467 register_action(editor, window, |editor, action, window, cx| {
468 if let Some(task) = editor.rename(action, window, cx) {
469 task.detach_and_notify_err(window, cx);
470 } else {
471 cx.propagate();
472 }
473 });
474 register_action(editor, window, |editor, action, window, cx| {
475 if let Some(task) = editor.confirm_rename(action, window, cx) {
476 task.detach_and_notify_err(window, cx);
477 } else {
478 cx.propagate();
479 }
480 });
481 register_action(editor, window, |editor, action, window, cx| {
482 if let Some(task) = editor.find_all_references(action, window, cx) {
483 task.detach_and_log_err(cx);
484 } else {
485 cx.propagate();
486 }
487 });
488 register_action(editor, window, Editor::show_signature_help);
489 register_action(editor, window, Editor::next_edit_prediction);
490 register_action(editor, window, Editor::previous_edit_prediction);
491 register_action(editor, window, Editor::show_inline_completion);
492 register_action(editor, window, Editor::context_menu_first);
493 register_action(editor, window, Editor::context_menu_prev);
494 register_action(editor, window, Editor::context_menu_next);
495 register_action(editor, window, Editor::context_menu_last);
496 register_action(editor, window, Editor::display_cursor_names);
497 register_action(editor, window, Editor::unique_lines_case_insensitive);
498 register_action(editor, window, Editor::unique_lines_case_sensitive);
499 register_action(editor, window, Editor::accept_partial_inline_completion);
500 register_action(editor, window, Editor::accept_edit_prediction);
501 register_action(editor, window, Editor::restore_file);
502 register_action(editor, window, Editor::git_restore);
503 register_action(editor, window, Editor::apply_all_diff_hunks);
504 register_action(editor, window, Editor::apply_selected_diff_hunks);
505 register_action(editor, window, Editor::open_active_item_in_terminal);
506 register_action(editor, window, Editor::reload_file);
507 register_action(editor, window, Editor::spawn_nearest_task);
508 register_action(editor, window, Editor::insert_uuid_v4);
509 register_action(editor, window, Editor::insert_uuid_v7);
510 register_action(editor, window, Editor::open_selections_in_multibuffer);
511 }
512
513 fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {
514 let position_map = layout.position_map.clone();
515 window.on_key_event({
516 let editor = self.editor.clone();
517 move |event: &ModifiersChangedEvent, phase, window, cx| {
518 if phase != DispatchPhase::Bubble {
519 return;
520 }
521 editor.update(cx, |editor, cx| {
522 let inlay_hint_settings = inlay_hint_settings(
523 editor.selections.newest_anchor().head(),
524 &editor.buffer.read(cx).snapshot(cx),
525 cx,
526 );
527
528 if let Some(inlay_modifiers) = inlay_hint_settings
529 .toggle_on_modifiers_press
530 .as_ref()
531 .filter(|modifiers| modifiers.modified())
532 {
533 editor.refresh_inlay_hints(
534 InlayHintRefreshReason::ModifiersChanged(
535 inlay_modifiers == &event.modifiers,
536 ),
537 cx,
538 );
539 }
540
541 if editor.hover_state.focused(window, cx) {
542 return;
543 }
544
545 editor.handle_modifiers_changed(event.modifiers, &position_map, window, cx);
546 })
547 }
548 });
549 }
550
551 fn mouse_left_down(
552 editor: &mut Editor,
553 event: &MouseDownEvent,
554 hovered_hunk: Option<Range<Anchor>>,
555 position_map: &PositionMap,
556 line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
557 window: &mut Window,
558 cx: &mut Context<Editor>,
559 ) {
560 if window.default_prevented() {
561 return;
562 }
563
564 let text_hitbox = &position_map.text_hitbox;
565 let gutter_hitbox = &position_map.gutter_hitbox;
566 let mut click_count = event.click_count;
567 let mut modifiers = event.modifiers;
568
569 if let Some(hovered_hunk) = hovered_hunk {
570 editor.toggle_single_diff_hunk(hovered_hunk, cx);
571 cx.notify();
572 return;
573 } else if gutter_hitbox.is_hovered(window) {
574 click_count = 3; // Simulate triple-click when clicking the gutter to select lines
575 } else if !text_hitbox.is_hovered(window) {
576 return;
577 }
578
579 let is_singleton = editor.buffer().read(cx).is_singleton();
580
581 if click_count == 2 && !is_singleton {
582 match EditorSettings::get_global(cx).double_click_in_multibuffer {
583 DoubleClickInMultibuffer::Select => {
584 // do nothing special on double click, all selection logic is below
585 }
586 DoubleClickInMultibuffer::Open => {
587 if modifiers.alt {
588 // if double click is made with alt, pretend it's a regular double click without opening and alt,
589 // and run the selection logic.
590 modifiers.alt = false;
591 } else {
592 let scroll_position_row =
593 position_map.scroll_pixel_position.y / position_map.line_height;
594 let display_row = (((event.position - gutter_hitbox.bounds.origin).y
595 + position_map.scroll_pixel_position.y)
596 / position_map.line_height)
597 as u32;
598 let multi_buffer_row = position_map
599 .snapshot
600 .display_point_to_point(
601 DisplayPoint::new(DisplayRow(display_row), 0),
602 Bias::Right,
603 )
604 .row;
605 let line_offset_from_top = display_row - scroll_position_row as u32;
606 // if double click is made without alt, open the corresponding excerp
607 editor.open_excerpts_common(
608 Some(JumpData::MultiBufferRow {
609 row: MultiBufferRow(multi_buffer_row),
610 line_offset_from_top,
611 }),
612 false,
613 window,
614 cx,
615 );
616 return;
617 }
618 }
619 }
620 }
621
622 let point_for_position = position_map.point_for_position(event.position);
623 let position = point_for_position.previous_valid;
624 if modifiers == COLUMNAR_SELECTION_MODIFIERS {
625 editor.select(
626 SelectPhase::BeginColumnar {
627 position,
628 reset: false,
629 goal_column: point_for_position.exact_unclipped.column(),
630 },
631 window,
632 cx,
633 );
634 } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.secondary()
635 {
636 editor.select(
637 SelectPhase::Extend {
638 position,
639 click_count,
640 },
641 window,
642 cx,
643 );
644 } else {
645 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
646 let multi_cursor_modifier = match multi_cursor_setting {
647 MultiCursorModifier::Alt => modifiers.alt,
648 MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
649 };
650 editor.select(
651 SelectPhase::Begin {
652 position,
653 add: multi_cursor_modifier,
654 click_count,
655 },
656 window,
657 cx,
658 );
659 }
660 cx.stop_propagation();
661
662 if !is_singleton {
663 let display_row = (((event.position - gutter_hitbox.bounds.origin).y
664 + position_map.scroll_pixel_position.y)
665 / position_map.line_height) as u32;
666 let multi_buffer_row = position_map
667 .snapshot
668 .display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right)
669 .row;
670 if line_numbers
671 .get(&MultiBufferRow(multi_buffer_row))
672 .and_then(|line_number| line_number.hitbox.as_ref())
673 .is_some_and(|hitbox| hitbox.contains(&event.position))
674 {
675 let scroll_position_row =
676 position_map.scroll_pixel_position.y / position_map.line_height;
677 let line_offset_from_top = display_row - scroll_position_row as u32;
678
679 editor.open_excerpts_common(
680 Some(JumpData::MultiBufferRow {
681 row: MultiBufferRow(multi_buffer_row),
682 line_offset_from_top,
683 }),
684 modifiers.alt,
685 window,
686 cx,
687 );
688 cx.stop_propagation();
689 }
690 }
691 }
692
693 fn mouse_right_down(
694 editor: &mut Editor,
695 event: &MouseDownEvent,
696 position_map: &PositionMap,
697 window: &mut Window,
698 cx: &mut Context<Editor>,
699 ) {
700 if !position_map.text_hitbox.is_hovered(window) {
701 return;
702 }
703 let point_for_position = position_map.point_for_position(event.position);
704 mouse_context_menu::deploy_context_menu(
705 editor,
706 Some(event.position),
707 point_for_position.previous_valid,
708 window,
709 cx,
710 );
711 cx.stop_propagation();
712 }
713
714 fn mouse_middle_down(
715 editor: &mut Editor,
716 event: &MouseDownEvent,
717 position_map: &PositionMap,
718 window: &mut Window,
719 cx: &mut Context<Editor>,
720 ) {
721 if !position_map.text_hitbox.is_hovered(window) || window.default_prevented() {
722 return;
723 }
724
725 let point_for_position = position_map.point_for_position(event.position);
726 let position = point_for_position.previous_valid;
727
728 editor.select(
729 SelectPhase::BeginColumnar {
730 position,
731 reset: true,
732 goal_column: point_for_position.exact_unclipped.column(),
733 },
734 window,
735 cx,
736 );
737 }
738
739 fn mouse_up(
740 editor: &mut Editor,
741 event: &MouseUpEvent,
742 position_map: &PositionMap,
743 window: &mut Window,
744 cx: &mut Context<Editor>,
745 ) {
746 let text_hitbox = &position_map.text_hitbox;
747 let end_selection = editor.has_pending_selection();
748 let pending_nonempty_selections = editor.has_pending_nonempty_selection();
749
750 if end_selection {
751 editor.select(SelectPhase::End, window, cx);
752 }
753
754 if end_selection && pending_nonempty_selections {
755 cx.stop_propagation();
756 } else if cfg!(any(target_os = "linux", target_os = "freebsd"))
757 && event.button == MouseButton::Middle
758 {
759 if !text_hitbox.is_hovered(window) || editor.read_only(cx) {
760 return;
761 }
762
763 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
764 if EditorSettings::get_global(cx).middle_click_paste {
765 if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
766 let point_for_position = position_map.point_for_position(event.position);
767 let position = point_for_position.previous_valid;
768
769 editor.select(
770 SelectPhase::Begin {
771 position,
772 add: false,
773 click_count: 1,
774 },
775 window,
776 cx,
777 );
778 editor.insert(&text, window, cx);
779 }
780 cx.stop_propagation()
781 }
782 }
783 }
784
785 fn click(
786 editor: &mut Editor,
787 event: &ClickEvent,
788 position_map: &PositionMap,
789 window: &mut Window,
790 cx: &mut Context<Editor>,
791 ) {
792 let text_hitbox = &position_map.text_hitbox;
793 let pending_nonempty_selections = editor.has_pending_nonempty_selection();
794
795 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
796 let multi_cursor_modifier = match multi_cursor_setting {
797 MultiCursorModifier::Alt => event.modifiers().secondary(),
798 MultiCursorModifier::CmdOrCtrl => event.modifiers().alt,
799 };
800
801 if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(window) {
802 let point = position_map.point_for_position(event.up.position);
803 editor.handle_click_hovered_link(point, event.modifiers(), window, cx);
804
805 cx.stop_propagation();
806 }
807 }
808
809 fn mouse_dragged(
810 editor: &mut Editor,
811 event: &MouseMoveEvent,
812 position_map: &PositionMap,
813 window: &mut Window,
814 cx: &mut Context<Editor>,
815 ) {
816 if !editor.has_pending_selection() {
817 return;
818 }
819
820 let text_bounds = position_map.text_hitbox.bounds;
821 let point_for_position = position_map.point_for_position(event.position);
822 let mut scroll_delta = gpui::Point::<f32>::default();
823 let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
824 let top = text_bounds.origin.y + vertical_margin;
825 let bottom = text_bounds.bottom_left().y - vertical_margin;
826 if event.position.y < top {
827 scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);
828 }
829 if event.position.y > bottom {
830 scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);
831 }
832
833 // We need horizontal width of text
834 let style = editor.style.clone().unwrap_or_default();
835 let font_id = window.text_system().resolve_font(&style.text.font());
836 let font_size = style.text.font_size.to_pixels(window.rem_size());
837 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
838
839 let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin;
840
841 let scroll_space: Pixels = scroll_margin_x * em_width;
842
843 let left = text_bounds.origin.x + scroll_space;
844 let right = text_bounds.top_right().x - scroll_space;
845
846 if event.position.x < left {
847 scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
848 }
849 if event.position.x > right {
850 scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
851 }
852
853 editor.select(
854 SelectPhase::Update {
855 position: point_for_position.previous_valid,
856 goal_column: point_for_position.exact_unclipped.column(),
857 scroll_delta,
858 },
859 window,
860 cx,
861 );
862 }
863
864 fn mouse_moved(
865 editor: &mut Editor,
866 event: &MouseMoveEvent,
867 position_map: &PositionMap,
868 window: &mut Window,
869 cx: &mut Context<Editor>,
870 ) {
871 let text_hitbox = &position_map.text_hitbox;
872 let gutter_hitbox = &position_map.gutter_hitbox;
873 let modifiers = event.modifiers;
874 let gutter_hovered = gutter_hitbox.is_hovered(window);
875 editor.set_gutter_hovered(gutter_hovered, cx);
876
877 // Don't trigger hover popover if mouse is hovering over context menu
878 if text_hitbox.is_hovered(window) {
879 let point_for_position = position_map.point_for_position(event.position);
880
881 editor.update_hovered_link(
882 point_for_position,
883 &position_map.snapshot,
884 modifiers,
885 window,
886 cx,
887 );
888
889 if let Some(point) = point_for_position.as_valid() {
890 let anchor = position_map
891 .snapshot
892 .buffer_snapshot
893 .anchor_before(point.to_offset(&position_map.snapshot, Bias::Left));
894 hover_at(editor, Some(anchor), window, cx);
895 Self::update_visible_cursor(editor, point, position_map, window, cx);
896 } else {
897 hover_at(editor, None, window, cx);
898 }
899 } else {
900 editor.hide_hovered_link(cx);
901 hover_at(editor, None, window, cx);
902 if gutter_hovered {
903 cx.stop_propagation();
904 }
905 }
906 }
907
908 fn update_visible_cursor(
909 editor: &mut Editor,
910 point: DisplayPoint,
911 position_map: &PositionMap,
912 window: &mut Window,
913 cx: &mut Context<Editor>,
914 ) {
915 let snapshot = &position_map.snapshot;
916 let Some(hub) = editor.collaboration_hub() else {
917 return;
918 };
919 let start = snapshot.display_snapshot.clip_point(
920 DisplayPoint::new(point.row(), point.column().saturating_sub(1)),
921 Bias::Left,
922 );
923 let end = snapshot.display_snapshot.clip_point(
924 DisplayPoint::new(
925 point.row(),
926 (point.column() + 1).min(snapshot.line_len(point.row())),
927 ),
928 Bias::Right,
929 );
930
931 let range = snapshot
932 .buffer_snapshot
933 .anchor_at(start.to_point(&snapshot.display_snapshot), Bias::Left)
934 ..snapshot
935 .buffer_snapshot
936 .anchor_at(end.to_point(&snapshot.display_snapshot), Bias::Right);
937
938 let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
939 return;
940 };
941 let key = crate::HoveredCursor {
942 replica_id: selection.replica_id,
943 selection_id: selection.selection.id,
944 };
945 editor.hovered_cursors.insert(
946 key.clone(),
947 cx.spawn_in(window, |editor, mut cx| async move {
948 cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
949 editor
950 .update(&mut cx, |editor, cx| {
951 editor.hovered_cursors.remove(&key);
952 cx.notify();
953 })
954 .ok();
955 }),
956 );
957 cx.notify()
958 }
959
960 #[allow(clippy::too_many_arguments)]
961 fn layout_selections(
962 &self,
963 start_anchor: Anchor,
964 end_anchor: Anchor,
965 local_selections: &[Selection<Point>],
966 snapshot: &EditorSnapshot,
967 start_row: DisplayRow,
968 end_row: DisplayRow,
969 window: &mut Window,
970 cx: &mut App,
971 ) -> (
972 Vec<(PlayerColor, Vec<SelectionLayout>)>,
973 BTreeMap<DisplayRow, bool>,
974 Option<DisplayPoint>,
975 ) {
976 let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
977 let mut active_rows = BTreeMap::new();
978 let mut newest_selection_head = None;
979 self.editor.update(cx, |editor, cx| {
980 if editor.show_local_selections {
981 let mut layouts = Vec::new();
982 let newest = editor.selections.newest(cx);
983 for selection in local_selections.iter().cloned() {
984 let is_empty = selection.start == selection.end;
985 let is_newest = selection == newest;
986
987 let layout = SelectionLayout::new(
988 selection,
989 editor.selections.line_mode,
990 editor.cursor_shape,
991 &snapshot.display_snapshot,
992 is_newest,
993 editor.leader_peer_id.is_none(),
994 None,
995 );
996 if is_newest {
997 newest_selection_head = Some(layout.head);
998 }
999
1000 for row in cmp::max(layout.active_rows.start.0, start_row.0)
1001 ..=cmp::min(layout.active_rows.end.0, end_row.0)
1002 {
1003 let contains_non_empty_selection =
1004 active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
1005 *contains_non_empty_selection |= !is_empty;
1006 }
1007 layouts.push(layout);
1008 }
1009
1010 let player = editor.current_user_player_color(cx);
1011 selections.push((player, layouts));
1012 }
1013
1014 if let Some(collaboration_hub) = &editor.collaboration_hub {
1015 // When following someone, render the local selections in their color.
1016 if let Some(leader_id) = editor.leader_peer_id {
1017 if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id)
1018 {
1019 if let Some(participant_index) = collaboration_hub
1020 .user_participant_indices(cx)
1021 .get(&collaborator.user_id)
1022 {
1023 if let Some((local_selection_style, _)) = selections.first_mut() {
1024 *local_selection_style = cx
1025 .theme()
1026 .players()
1027 .color_for_participant(participant_index.0);
1028 }
1029 }
1030 }
1031 }
1032
1033 let mut remote_selections = HashMap::default();
1034 for selection in snapshot.remote_selections_in_range(
1035 &(start_anchor..end_anchor),
1036 collaboration_hub.as_ref(),
1037 cx,
1038 ) {
1039 let selection_style =
1040 Self::get_participant_color(selection.participant_index, cx);
1041
1042 // Don't re-render the leader's selections, since the local selections
1043 // match theirs.
1044 if Some(selection.peer_id) == editor.leader_peer_id {
1045 continue;
1046 }
1047 let key = HoveredCursor {
1048 replica_id: selection.replica_id,
1049 selection_id: selection.selection.id,
1050 };
1051
1052 let is_shown =
1053 editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
1054
1055 remote_selections
1056 .entry(selection.replica_id)
1057 .or_insert((selection_style, Vec::new()))
1058 .1
1059 .push(SelectionLayout::new(
1060 selection.selection,
1061 selection.line_mode,
1062 selection.cursor_shape,
1063 &snapshot.display_snapshot,
1064 false,
1065 false,
1066 if is_shown { selection.user_name } else { None },
1067 ));
1068 }
1069
1070 selections.extend(remote_selections.into_values());
1071 } else if !editor.is_focused(window) && editor.show_cursor_when_unfocused {
1072 let layouts = snapshot
1073 .buffer_snapshot
1074 .selections_in_range(&(start_anchor..end_anchor), true)
1075 .map(move |(_, line_mode, cursor_shape, selection)| {
1076 SelectionLayout::new(
1077 selection,
1078 line_mode,
1079 cursor_shape,
1080 &snapshot.display_snapshot,
1081 false,
1082 false,
1083 None,
1084 )
1085 })
1086 .collect::<Vec<_>>();
1087 let player = editor.current_user_player_color(cx);
1088 selections.push((player, layouts));
1089 }
1090 });
1091 (selections, active_rows, newest_selection_head)
1092 }
1093
1094 fn collect_cursors(
1095 &self,
1096 snapshot: &EditorSnapshot,
1097 cx: &mut App,
1098 ) -> Vec<(DisplayPoint, Hsla)> {
1099 let editor = self.editor.read(cx);
1100 let mut cursors = Vec::new();
1101 let mut skip_local = false;
1102 let mut add_cursor = |anchor: Anchor, color| {
1103 cursors.push((anchor.to_display_point(&snapshot.display_snapshot), color));
1104 };
1105 // Remote cursors
1106 if let Some(collaboration_hub) = &editor.collaboration_hub {
1107 for remote_selection in snapshot.remote_selections_in_range(
1108 &(Anchor::min()..Anchor::max()),
1109 collaboration_hub.deref(),
1110 cx,
1111 ) {
1112 let color = Self::get_participant_color(remote_selection.participant_index, cx);
1113 add_cursor(remote_selection.selection.head(), color.cursor);
1114 if Some(remote_selection.peer_id) == editor.leader_peer_id {
1115 skip_local = true;
1116 }
1117 }
1118 }
1119 // Local cursors
1120 if !skip_local {
1121 let color = cx.theme().players().local().cursor;
1122 editor.selections.disjoint.iter().for_each(|selection| {
1123 add_cursor(selection.head(), color);
1124 });
1125 if let Some(ref selection) = editor.selections.pending_anchor() {
1126 add_cursor(selection.head(), color);
1127 }
1128 }
1129 cursors
1130 }
1131
1132 #[allow(clippy::too_many_arguments)]
1133 fn layout_visible_cursors(
1134 &self,
1135 snapshot: &EditorSnapshot,
1136 selections: &[(PlayerColor, Vec<SelectionLayout>)],
1137 block_start_rows: &HashSet<DisplayRow>,
1138 visible_display_row_range: Range<DisplayRow>,
1139 line_layouts: &[LineWithInvisibles],
1140 text_hitbox: &Hitbox,
1141 content_origin: gpui::Point<Pixels>,
1142 scroll_position: gpui::Point<f32>,
1143 scroll_pixel_position: gpui::Point<Pixels>,
1144 line_height: Pixels,
1145 em_width: Pixels,
1146 em_advance: Pixels,
1147 autoscroll_containing_element: bool,
1148 window: &mut Window,
1149 cx: &mut App,
1150 ) -> Vec<CursorLayout> {
1151 let mut autoscroll_bounds = None;
1152 let cursor_layouts = self.editor.update(cx, |editor, cx| {
1153 let mut cursors = Vec::new();
1154
1155 let show_local_cursors = editor.show_local_cursors(window, cx);
1156
1157 for (player_color, selections) in selections {
1158 for selection in selections {
1159 let cursor_position = selection.head;
1160
1161 let in_range = visible_display_row_range.contains(&cursor_position.row());
1162 if (selection.is_local && !show_local_cursors)
1163 || !in_range
1164 || block_start_rows.contains(&cursor_position.row())
1165 {
1166 continue;
1167 }
1168
1169 let cursor_row_layout = &line_layouts
1170 [cursor_position.row().minus(visible_display_row_range.start) as usize];
1171 let cursor_column = cursor_position.column() as usize;
1172
1173 let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
1174 let mut block_width =
1175 cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
1176 if block_width == Pixels::ZERO {
1177 block_width = em_advance;
1178 }
1179 let block_text = if let CursorShape::Block = selection.cursor_shape {
1180 snapshot
1181 .grapheme_at(cursor_position)
1182 .or_else(|| {
1183 if cursor_column == 0 {
1184 snapshot.placeholder_text().and_then(|s| {
1185 s.graphemes(true).next().map(|s| s.to_string().into())
1186 })
1187 } else {
1188 None
1189 }
1190 })
1191 .and_then(|text| {
1192 let len = text.len();
1193
1194 let font = cursor_row_layout
1195 .font_id_for_index(cursor_column)
1196 .and_then(|cursor_font_id| {
1197 window.text_system().get_font_for_id(cursor_font_id)
1198 })
1199 .unwrap_or(self.style.text.font());
1200
1201 // Invert the text color for the block cursor. Ensure that the text
1202 // color is opaque enough to be visible against the background color.
1203 //
1204 // 0.75 is an arbitrary threshold to determine if the background color is
1205 // opaque enough to use as a text color.
1206 //
1207 // TODO: In the future we should ensure themes have a `text_inverse` color.
1208 let color = if cx.theme().colors().editor_background.a < 0.75 {
1209 match cx.theme().appearance {
1210 Appearance::Dark => Hsla::black(),
1211 Appearance::Light => Hsla::white(),
1212 }
1213 } else {
1214 cx.theme().colors().editor_background
1215 };
1216
1217 window
1218 .text_system()
1219 .shape_line(
1220 text,
1221 cursor_row_layout.font_size,
1222 &[TextRun {
1223 len,
1224 font,
1225 color,
1226 background_color: None,
1227 strikethrough: None,
1228 underline: None,
1229 }],
1230 )
1231 .log_err()
1232 })
1233 } else {
1234 None
1235 };
1236
1237 let x = cursor_character_x - scroll_pixel_position.x;
1238 let y = (cursor_position.row().as_f32()
1239 - scroll_pixel_position.y / line_height)
1240 * line_height;
1241 if selection.is_newest {
1242 editor.pixel_position_of_newest_cursor = Some(point(
1243 text_hitbox.origin.x + x + block_width / 2.,
1244 text_hitbox.origin.y + y + line_height / 2.,
1245 ));
1246
1247 if autoscroll_containing_element {
1248 let top = text_hitbox.origin.y
1249 + (cursor_position.row().as_f32() - scroll_position.y - 3.).max(0.)
1250 * line_height;
1251 let left = text_hitbox.origin.x
1252 + (cursor_position.column() as f32 - scroll_position.x - 3.)
1253 .max(0.)
1254 * em_width;
1255
1256 let bottom = text_hitbox.origin.y
1257 + (cursor_position.row().as_f32() - scroll_position.y + 4.)
1258 * line_height;
1259 let right = text_hitbox.origin.x
1260 + (cursor_position.column() as f32 - scroll_position.x + 4.)
1261 * em_width;
1262
1263 autoscroll_bounds =
1264 Some(Bounds::from_corners(point(left, top), point(right, bottom)))
1265 }
1266 }
1267
1268 let mut cursor = CursorLayout {
1269 color: player_color.cursor,
1270 block_width,
1271 origin: point(x, y),
1272 line_height,
1273 shape: selection.cursor_shape,
1274 block_text,
1275 cursor_name: None,
1276 };
1277 let cursor_name = selection.user_name.clone().map(|name| CursorName {
1278 string: name,
1279 color: self.style.background,
1280 is_top_row: cursor_position.row().0 == 0,
1281 });
1282 cursor.layout(content_origin, cursor_name, window, cx);
1283 cursors.push(cursor);
1284 }
1285 }
1286
1287 cursors
1288 });
1289
1290 if let Some(bounds) = autoscroll_bounds {
1291 window.request_autoscroll(bounds);
1292 }
1293
1294 cursor_layouts
1295 }
1296
1297 fn layout_scrollbars(
1298 &self,
1299 snapshot: &EditorSnapshot,
1300 scrollbar_range_data: ScrollbarRangeData,
1301 scroll_position: gpui::Point<f32>,
1302 non_visible_cursors: bool,
1303 window: &mut Window,
1304 cx: &mut App,
1305 ) -> AxisPair<Option<ScrollbarLayout>> {
1306 let letter_size = scrollbar_range_data.letter_size;
1307 let text_units_per_page = axis_pair(
1308 scrollbar_range_data.scrollbar_bounds.size.width / letter_size.width,
1309 scrollbar_range_data.scrollbar_bounds.size.height / letter_size.height,
1310 );
1311
1312 let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
1313 let show_scrollbars = self.editor.read(cx).show_scrollbars
1314 && match scrollbar_settings.show {
1315 ShowScrollbar::Auto => {
1316 let editor = self.editor.read(cx);
1317 let is_singleton = editor.is_singleton(cx);
1318 // Git
1319 (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_diff_hunks())
1320 ||
1321 // Buffer Search Results
1322 (is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
1323 ||
1324 // Selected Text Occurrences
1325 (is_singleton && scrollbar_settings.selected_text && editor.has_background_highlights::<SelectedTextHighlight>())
1326 ||
1327 // Selected Symbol Occurrences
1328 (is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
1329 ||
1330 // Diagnostics
1331 (is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot.has_diagnostics())
1332 ||
1333 // Cursors out of sight
1334 non_visible_cursors
1335 ||
1336 // Scrollmanager
1337 editor.scroll_manager.scrollbars_visible()
1338 }
1339 ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
1340 ShowScrollbar::Always => true,
1341 ShowScrollbar::Never => false,
1342 };
1343
1344 let axes: AxisPair<bool> = scrollbar_settings.axes.into();
1345
1346 if snapshot.mode != EditorMode::Full {
1347 return axis_pair(None, None);
1348 }
1349
1350 let visible_range = axis_pair(
1351 axes.horizontal
1352 .then(|| scroll_position.x..scroll_position.x + text_units_per_page.horizontal),
1353 axes.vertical
1354 .then(|| scroll_position.y..scroll_position.y + text_units_per_page.vertical),
1355 );
1356
1357 // If a drag took place after we started dragging the scrollbar,
1358 // cancel the scrollbar drag.
1359 if cx.has_active_drag() {
1360 self.editor.update(cx, |editor, cx| {
1361 editor
1362 .scroll_manager
1363 .set_is_dragging_scrollbar(Axis::Horizontal, false, cx);
1364 editor
1365 .scroll_manager
1366 .set_is_dragging_scrollbar(Axis::Vertical, false, cx);
1367 });
1368 }
1369
1370 let text_bounds = scrollbar_range_data.scrollbar_bounds;
1371
1372 let track_bounds = axis_pair(
1373 axes.horizontal.then(|| {
1374 Bounds::from_corners(
1375 point(
1376 text_bounds.bottom_left().x,
1377 text_bounds.bottom_left().y - self.style.scrollbar_width,
1378 ),
1379 point(
1380 text_bounds.bottom_right().x
1381 - if axes.vertical {
1382 self.style.scrollbar_width
1383 } else {
1384 px(0.)
1385 },
1386 text_bounds.bottom_right().y,
1387 ),
1388 )
1389 }),
1390 axes.vertical.then(|| {
1391 Bounds::from_corners(
1392 point(self.scrollbar_left(&text_bounds), text_bounds.origin.y),
1393 text_bounds.bottom_right(),
1394 )
1395 }),
1396 );
1397
1398 let scroll_range_size = scrollbar_range_data.scroll_range.size;
1399 let total_text_units = axis_pair(
1400 Some(scroll_range_size.width / letter_size.width),
1401 Some(scroll_range_size.height / letter_size.height),
1402 );
1403
1404 let thumb_size = axis_pair(
1405 total_text_units
1406 .horizontal
1407 .zip(track_bounds.horizontal)
1408 .and_then(|(total_text_units_x, track_bounds_x)| {
1409 if text_units_per_page.horizontal >= total_text_units_x {
1410 return None;
1411 }
1412 if track_bounds_x.size.width < px(MIN_SCROLL_THUMB_SIZE) {
1413 return Some(track_bounds_x.size.width);
1414 }
1415 let thumb_size = track_bounds_x.size.width
1416 * (text_units_per_page.horizontal / total_text_units_x);
1417 Some(thumb_size.clamp(px(MIN_SCROLL_THUMB_SIZE), track_bounds_x.size.width))
1418 }),
1419 total_text_units.vertical.zip(track_bounds.vertical).map(
1420 |(total_text_units_y, track_bounds_y)| {
1421 if track_bounds_y.size.height < px(MIN_SCROLL_THUMB_SIZE) {
1422 return track_bounds_y.size.height;
1423 }
1424 let thumb_size = track_bounds_y.size.height
1425 * (text_units_per_page.vertical / total_text_units_y);
1426 thumb_size.clamp(px(MIN_SCROLL_THUMB_SIZE), track_bounds_y.size.height)
1427 },
1428 ),
1429 );
1430
1431 // NOTE: Space not taken by track bounds divided by text units not on screen
1432 let text_unit_size = axis_pair(
1433 thumb_size
1434 .horizontal
1435 .zip(track_bounds.horizontal)
1436 .zip(total_text_units.horizontal)
1437 .map(|((thumb_size, track_bounds), total_text_units)| {
1438 (track_bounds.size.width - thumb_size)
1439 / (total_text_units - text_units_per_page.horizontal).max(0.)
1440 }),
1441 thumb_size
1442 .vertical
1443 .zip(track_bounds.vertical)
1444 .zip(total_text_units.vertical)
1445 .map(|((thumb_size, track_bounds), total_text_units)| {
1446 (track_bounds.size.height - thumb_size)
1447 / (total_text_units - text_units_per_page.vertical).max(0.)
1448 }),
1449 );
1450
1451 let horizontal_scrollbar = track_bounds
1452 .horizontal
1453 .zip(visible_range.horizontal)
1454 .zip(text_unit_size.horizontal)
1455 .zip(thumb_size.horizontal)
1456 .map(
1457 |(((track_bounds, visible_range), text_unit_size), thumb_size)| ScrollbarLayout {
1458 hitbox: window.insert_hitbox(track_bounds, false),
1459 visible_range,
1460 text_unit_size,
1461 visible: show_scrollbars,
1462 thumb_size,
1463 axis: Axis::Horizontal,
1464 },
1465 );
1466
1467 let vertical_scrollbar = track_bounds
1468 .vertical
1469 .zip(visible_range.vertical)
1470 .zip(text_unit_size.vertical)
1471 .zip(thumb_size.vertical)
1472 .map(
1473 |(((track_bounds, visible_range), text_unit_size), thumb_size)| ScrollbarLayout {
1474 hitbox: window.insert_hitbox(track_bounds, false),
1475 visible_range,
1476 text_unit_size,
1477 visible: show_scrollbars,
1478 thumb_size,
1479 axis: Axis::Vertical,
1480 },
1481 );
1482
1483 axis_pair(horizontal_scrollbar, vertical_scrollbar)
1484 }
1485
1486 #[allow(clippy::too_many_arguments)]
1487 fn prepaint_crease_toggles(
1488 &self,
1489 crease_toggles: &mut [Option<AnyElement>],
1490 line_height: Pixels,
1491 gutter_dimensions: &GutterDimensions,
1492 gutter_settings: crate::editor_settings::Gutter,
1493 scroll_pixel_position: gpui::Point<Pixels>,
1494 gutter_hitbox: &Hitbox,
1495 window: &mut Window,
1496 cx: &mut App,
1497 ) {
1498 for (ix, crease_toggle) in crease_toggles.iter_mut().enumerate() {
1499 if let Some(crease_toggle) = crease_toggle {
1500 debug_assert!(gutter_settings.folds);
1501 let available_space = size(
1502 AvailableSpace::MinContent,
1503 AvailableSpace::Definite(line_height * 0.55),
1504 );
1505 let crease_toggle_size = crease_toggle.layout_as_root(available_space, window, cx);
1506
1507 let position = point(
1508 gutter_dimensions.width - gutter_dimensions.right_padding,
1509 ix as f32 * line_height - (scroll_pixel_position.y % line_height),
1510 );
1511 let centering_offset = point(
1512 (gutter_dimensions.fold_area_width() - crease_toggle_size.width) / 2.,
1513 (line_height - crease_toggle_size.height) / 2.,
1514 );
1515 let origin = gutter_hitbox.origin + position + centering_offset;
1516 crease_toggle.prepaint_as_root(origin, available_space, window, cx);
1517 }
1518 }
1519 }
1520
1521 #[allow(clippy::too_many_arguments)]
1522 fn prepaint_crease_trailers(
1523 &self,
1524 trailers: Vec<Option<AnyElement>>,
1525 lines: &[LineWithInvisibles],
1526 line_height: Pixels,
1527 content_origin: gpui::Point<Pixels>,
1528 scroll_pixel_position: gpui::Point<Pixels>,
1529 em_width: Pixels,
1530 window: &mut Window,
1531 cx: &mut App,
1532 ) -> Vec<Option<CreaseTrailerLayout>> {
1533 trailers
1534 .into_iter()
1535 .enumerate()
1536 .map(|(ix, element)| {
1537 let mut element = element?;
1538 let available_space = size(
1539 AvailableSpace::MinContent,
1540 AvailableSpace::Definite(line_height),
1541 );
1542 let size = element.layout_as_root(available_space, window, cx);
1543
1544 let line = &lines[ix];
1545 let padding = if line.width == Pixels::ZERO {
1546 Pixels::ZERO
1547 } else {
1548 4. * em_width
1549 };
1550 let position = point(
1551 scroll_pixel_position.x + line.width + padding,
1552 ix as f32 * line_height - (scroll_pixel_position.y % line_height),
1553 );
1554 let centering_offset = point(px(0.), (line_height - size.height) / 2.);
1555 let origin = content_origin + position + centering_offset;
1556 element.prepaint_as_root(origin, available_space, window, cx);
1557 Some(CreaseTrailerLayout {
1558 element,
1559 bounds: Bounds::new(origin, size),
1560 })
1561 })
1562 .collect()
1563 }
1564
1565 // Folds contained in a hunk are ignored apart from shrinking visual size
1566 // If a fold contains any hunks then that fold line is marked as modified
1567 fn layout_gutter_diff_hunks(
1568 &self,
1569 line_height: Pixels,
1570 gutter_hitbox: &Hitbox,
1571 display_rows: Range<DisplayRow>,
1572 snapshot: &EditorSnapshot,
1573 window: &mut Window,
1574 cx: &mut App,
1575 ) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
1576 let folded_buffers = self.editor.read(cx).folded_buffers(cx);
1577 let mut display_hunks = snapshot
1578 .display_diff_hunks_for_rows(display_rows, folded_buffers)
1579 .map(|hunk| (hunk, None))
1580 .collect::<Vec<_>>();
1581 let git_gutter_setting = ProjectSettings::get_global(cx)
1582 .git
1583 .git_gutter
1584 .unwrap_or_default();
1585 if let GitGutterSetting::TrackedFiles = git_gutter_setting {
1586 for (hunk, hitbox) in &mut display_hunks {
1587 if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
1588 let hunk_bounds =
1589 Self::diff_hunk_bounds(snapshot, line_height, gutter_hitbox.bounds, hunk);
1590 *hitbox = Some(window.insert_hitbox(hunk_bounds, true));
1591 }
1592 }
1593 }
1594
1595 display_hunks
1596 }
1597
1598 #[allow(clippy::too_many_arguments)]
1599 fn layout_inline_diagnostics(
1600 &self,
1601 line_layouts: &[LineWithInvisibles],
1602 crease_trailers: &[Option<CreaseTrailerLayout>],
1603 content_origin: gpui::Point<Pixels>,
1604 scroll_pixel_position: gpui::Point<Pixels>,
1605 inline_completion_popover_origin: Option<gpui::Point<Pixels>>,
1606 start_row: DisplayRow,
1607 end_row: DisplayRow,
1608 line_height: Pixels,
1609 em_width: Pixels,
1610 style: &EditorStyle,
1611 window: &mut Window,
1612 cx: &mut App,
1613 ) -> HashMap<DisplayRow, AnyElement> {
1614 let max_severity = ProjectSettings::get_global(cx)
1615 .diagnostics
1616 .inline
1617 .max_severity
1618 .map_or(DiagnosticSeverity::HINT, |severity| match severity {
1619 project_settings::DiagnosticSeverity::Error => DiagnosticSeverity::ERROR,
1620 project_settings::DiagnosticSeverity::Warning => DiagnosticSeverity::WARNING,
1621 project_settings::DiagnosticSeverity::Info => DiagnosticSeverity::INFORMATION,
1622 project_settings::DiagnosticSeverity::Hint => DiagnosticSeverity::HINT,
1623 });
1624
1625 let active_diagnostics_group = self
1626 .editor
1627 .read(cx)
1628 .active_diagnostics
1629 .as_ref()
1630 .map(|active_diagnostics| active_diagnostics.group_id);
1631
1632 let diagnostics_by_rows = self.editor.update(cx, |editor, cx| {
1633 let snapshot = editor.snapshot(window, cx);
1634 editor
1635 .inline_diagnostics
1636 .iter()
1637 .filter(|(_, diagnostic)| diagnostic.severity <= max_severity)
1638 .filter(|(_, diagnostic)| match active_diagnostics_group {
1639 Some(active_diagnostics_group) => {
1640 // Active diagnostics are all shown in the editor already, no need to display them inline
1641 diagnostic.group_id != active_diagnostics_group
1642 }
1643 None => true,
1644 })
1645 .map(|(point, diag)| (point.to_display_point(&snapshot), diag.clone()))
1646 .skip_while(|(point, _)| point.row() < start_row)
1647 .take_while(|(point, _)| point.row() < end_row)
1648 .fold(HashMap::default(), |mut acc, (point, diagnostic)| {
1649 acc.entry(point.row())
1650 .or_insert_with(Vec::new)
1651 .push(diagnostic);
1652 acc
1653 })
1654 });
1655
1656 if diagnostics_by_rows.is_empty() {
1657 return HashMap::default();
1658 }
1659
1660 let severity_to_color = |sev: &DiagnosticSeverity| match sev {
1661 &DiagnosticSeverity::ERROR => Color::Error,
1662 &DiagnosticSeverity::WARNING => Color::Warning,
1663 &DiagnosticSeverity::INFORMATION => Color::Info,
1664 &DiagnosticSeverity::HINT => Color::Hint,
1665 _ => Color::Error,
1666 };
1667
1668 let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width;
1669 let min_x = ProjectSettings::get_global(cx)
1670 .diagnostics
1671 .inline
1672 .min_column as f32
1673 * em_width;
1674
1675 let mut elements = HashMap::default();
1676 for (row, mut diagnostics) in diagnostics_by_rows {
1677 diagnostics.sort_by_key(|diagnostic| {
1678 (
1679 diagnostic.severity,
1680 std::cmp::Reverse(diagnostic.is_primary),
1681 diagnostic.start.row,
1682 diagnostic.start.column,
1683 )
1684 });
1685
1686 let Some(diagnostic_to_render) = diagnostics
1687 .iter()
1688 .find(|diagnostic| diagnostic.is_primary)
1689 .or_else(|| diagnostics.first())
1690 else {
1691 continue;
1692 };
1693
1694 let pos_y = content_origin.y
1695 + line_height * (row.0 as f32 - scroll_pixel_position.y / line_height);
1696
1697 let window_ix = row.0.saturating_sub(start_row.0) as usize;
1698 let pos_x = {
1699 let crease_trailer_layout = &crease_trailers[window_ix];
1700 let line_layout = &line_layouts[window_ix];
1701
1702 let line_end = if let Some(crease_trailer) = crease_trailer_layout {
1703 crease_trailer.bounds.right()
1704 } else {
1705 content_origin.x - scroll_pixel_position.x + line_layout.width
1706 };
1707
1708 let padded_line = line_end + padding;
1709 let min_start = content_origin.x - scroll_pixel_position.x + min_x;
1710
1711 cmp::max(padded_line, min_start)
1712 };
1713
1714 let behind_inline_completion_popover = inline_completion_popover_origin
1715 .as_ref()
1716 .map_or(false, |inline_completion_popover_origin| {
1717 (pos_y..pos_y + line_height).contains(&inline_completion_popover_origin.y)
1718 });
1719 let opacity = if behind_inline_completion_popover {
1720 0.5
1721 } else {
1722 1.0
1723 };
1724
1725 let mut element = h_flex()
1726 .id(("diagnostic", row.0))
1727 .h(line_height)
1728 .w_full()
1729 .px_1()
1730 .rounded_sm()
1731 .opacity(opacity)
1732 .bg(severity_to_color(&diagnostic_to_render.severity)
1733 .color(cx)
1734 .opacity(0.05))
1735 .text_color(severity_to_color(&diagnostic_to_render.severity).color(cx))
1736 .text_sm()
1737 .font_family(style.text.font().family)
1738 .child(diagnostic_to_render.message.clone())
1739 .into_any();
1740
1741 element.prepaint_as_root(point(pos_x, pos_y), AvailableSpace::min_size(), window, cx);
1742
1743 elements.insert(row, element);
1744 }
1745
1746 elements
1747 }
1748
1749 #[allow(clippy::too_many_arguments)]
1750 fn layout_inline_blame(
1751 &self,
1752 display_row: DisplayRow,
1753 row_info: &RowInfo,
1754 line_layout: &LineWithInvisibles,
1755 crease_trailer: Option<&CreaseTrailerLayout>,
1756 em_width: Pixels,
1757 content_origin: gpui::Point<Pixels>,
1758 scroll_pixel_position: gpui::Point<Pixels>,
1759 line_height: Pixels,
1760 window: &mut Window,
1761 cx: &mut App,
1762 ) -> Option<AnyElement> {
1763 if !self
1764 .editor
1765 .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx))
1766 {
1767 return None;
1768 }
1769
1770 let editor = self.editor.read(cx);
1771 let blame = editor.blame.clone()?;
1772 let padding = {
1773 const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
1774 const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.;
1775
1776 let mut padding = INLINE_BLAME_PADDING_EM_WIDTHS;
1777
1778 if let Some(inline_completion) = editor.active_inline_completion.as_ref() {
1779 match &inline_completion.completion {
1780 InlineCompletion::Edit {
1781 display_mode: EditDisplayMode::TabAccept,
1782 ..
1783 } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS,
1784 _ => {}
1785 }
1786 }
1787
1788 padding * em_width
1789 };
1790
1791 let blame_entry = blame
1792 .update(cx, |blame, cx| {
1793 blame.blame_for_rows(&[*row_info], cx).next()
1794 })
1795 .flatten()?;
1796
1797 let mut element =
1798 render_inline_blame_entry(self.editor.clone(), &blame, blame_entry, &self.style, cx);
1799
1800 let start_y = content_origin.y
1801 + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
1802
1803 let start_x = {
1804 let line_end = if let Some(crease_trailer) = crease_trailer {
1805 crease_trailer.bounds.right()
1806 } else {
1807 content_origin.x - scroll_pixel_position.x + line_layout.width
1808 };
1809
1810 let padded_line_end = line_end + padding;
1811
1812 let min_column_in_pixels = ProjectSettings::get_global(cx)
1813 .git
1814 .inline_blame
1815 .and_then(|settings| settings.min_column)
1816 .map(|col| self.column_pixels(col as usize, window, cx))
1817 .unwrap_or(px(0.));
1818 let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
1819
1820 cmp::max(padded_line_end, min_start)
1821 };
1822
1823 let absolute_offset = point(start_x, start_y);
1824 element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
1825
1826 Some(element)
1827 }
1828
1829 #[allow(clippy::too_many_arguments)]
1830 fn layout_blame_entries(
1831 &self,
1832 buffer_rows: &[RowInfo],
1833 em_width: Pixels,
1834 scroll_position: gpui::Point<f32>,
1835 line_height: Pixels,
1836 gutter_hitbox: &Hitbox,
1837 max_width: Option<Pixels>,
1838 window: &mut Window,
1839 cx: &mut App,
1840 ) -> Option<Vec<AnyElement>> {
1841 if !self
1842 .editor
1843 .update(cx, |editor, cx| editor.render_git_blame_gutter(cx))
1844 {
1845 return None;
1846 }
1847
1848 let blame = self.editor.read(cx).blame.clone()?;
1849 let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
1850 blame.blame_for_rows(buffer_rows, cx).collect()
1851 });
1852
1853 let width = if let Some(max_width) = max_width {
1854 AvailableSpace::Definite(max_width)
1855 } else {
1856 AvailableSpace::MaxContent
1857 };
1858 let scroll_top = scroll_position.y * line_height;
1859 let start_x = em_width;
1860
1861 let mut last_used_color: Option<(PlayerColor, Oid)> = None;
1862
1863 let shaped_lines = blamed_rows
1864 .into_iter()
1865 .enumerate()
1866 .flat_map(|(ix, blame_entry)| {
1867 if let Some(blame_entry) = blame_entry {
1868 let mut element = render_blame_entry(
1869 ix,
1870 &blame,
1871 blame_entry,
1872 &self.style,
1873 &mut last_used_color,
1874 self.editor.clone(),
1875 cx,
1876 );
1877
1878 let start_y = ix as f32 * line_height - (scroll_top % line_height);
1879 let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
1880
1881 element.prepaint_as_root(
1882 absolute_offset,
1883 size(width, AvailableSpace::MinContent),
1884 window,
1885 cx,
1886 );
1887
1888 Some(element)
1889 } else {
1890 None
1891 }
1892 })
1893 .collect();
1894
1895 Some(shaped_lines)
1896 }
1897
1898 #[allow(clippy::too_many_arguments)]
1899 fn layout_indent_guides(
1900 &self,
1901 content_origin: gpui::Point<Pixels>,
1902 text_origin: gpui::Point<Pixels>,
1903 visible_buffer_range: Range<MultiBufferRow>,
1904 scroll_pixel_position: gpui::Point<Pixels>,
1905 line_height: Pixels,
1906 snapshot: &DisplaySnapshot,
1907 window: &mut Window,
1908 cx: &mut App,
1909 ) -> Option<Vec<IndentGuideLayout>> {
1910 let indent_guides = self.editor.update(cx, |editor, cx| {
1911 editor.indent_guides(visible_buffer_range, snapshot, cx)
1912 })?;
1913
1914 let active_indent_guide_indices = self.editor.update(cx, |editor, cx| {
1915 editor
1916 .find_active_indent_guide_indices(&indent_guides, snapshot, window, cx)
1917 .unwrap_or_default()
1918 });
1919
1920 Some(
1921 indent_guides
1922 .into_iter()
1923 .enumerate()
1924 .filter_map(|(i, indent_guide)| {
1925 let single_indent_width =
1926 self.column_pixels(indent_guide.tab_size as usize, window, cx);
1927 let total_width = single_indent_width * indent_guide.depth as f32;
1928 let start_x = content_origin.x + total_width - scroll_pixel_position.x;
1929 if start_x >= text_origin.x {
1930 let (offset_y, length) = Self::calculate_indent_guide_bounds(
1931 indent_guide.start_row..indent_guide.end_row,
1932 line_height,
1933 snapshot,
1934 );
1935
1936 let start_y = content_origin.y + offset_y - scroll_pixel_position.y;
1937
1938 Some(IndentGuideLayout {
1939 origin: point(start_x, start_y),
1940 length,
1941 single_indent_width,
1942 depth: indent_guide.depth,
1943 active: active_indent_guide_indices.contains(&i),
1944 settings: indent_guide.settings,
1945 })
1946 } else {
1947 None
1948 }
1949 })
1950 .collect(),
1951 )
1952 }
1953
1954 fn calculate_indent_guide_bounds(
1955 row_range: Range<MultiBufferRow>,
1956 line_height: Pixels,
1957 snapshot: &DisplaySnapshot,
1958 ) -> (gpui::Pixels, gpui::Pixels) {
1959 let start_point = Point::new(row_range.start.0, 0);
1960 let end_point = Point::new(row_range.end.0, 0);
1961
1962 let row_range = start_point.to_display_point(snapshot).row()
1963 ..end_point.to_display_point(snapshot).row();
1964
1965 let mut prev_line = start_point;
1966 prev_line.row = prev_line.row.saturating_sub(1);
1967 let prev_line = prev_line.to_display_point(snapshot).row();
1968
1969 let mut cons_line = end_point;
1970 cons_line.row += 1;
1971 let cons_line = cons_line.to_display_point(snapshot).row();
1972
1973 let mut offset_y = row_range.start.0 as f32 * line_height;
1974 let mut length = (cons_line.0.saturating_sub(row_range.start.0)) as f32 * line_height;
1975
1976 // If we are at the end of the buffer, ensure that the indent guide extends to the end of the line.
1977 if row_range.end == cons_line {
1978 length += line_height;
1979 }
1980
1981 // If there is a block (e.g. diagnostic) in between the start of the indent guide and the line above,
1982 // we want to extend the indent guide to the start of the block.
1983 let mut block_height = 0;
1984 let mut block_offset = 0;
1985 let mut found_excerpt_header = false;
1986 for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
1987 if matches!(block, Block::ExcerptBoundary { .. }) {
1988 found_excerpt_header = true;
1989 break;
1990 }
1991 block_offset += block.height();
1992 block_height += block.height();
1993 }
1994 if !found_excerpt_header {
1995 offset_y -= block_offset as f32 * line_height;
1996 length += block_height as f32 * line_height;
1997 }
1998
1999 // If there is a block (e.g. diagnostic) at the end of an multibuffer excerpt,
2000 // we want to ensure that the indent guide stops before the excerpt header.
2001 let mut block_height = 0;
2002 let mut found_excerpt_header = false;
2003 for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
2004 if matches!(block, Block::ExcerptBoundary { .. }) {
2005 found_excerpt_header = true;
2006 }
2007 block_height += block.height();
2008 }
2009 if found_excerpt_header {
2010 length -= block_height as f32 * line_height;
2011 }
2012
2013 (offset_y, length)
2014 }
2015
2016 #[allow(clippy::too_many_arguments)]
2017 fn layout_run_indicators(
2018 &self,
2019 line_height: Pixels,
2020 range: Range<DisplayRow>,
2021 scroll_pixel_position: gpui::Point<Pixels>,
2022 gutter_dimensions: &GutterDimensions,
2023 gutter_hitbox: &Hitbox,
2024 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
2025 snapshot: &EditorSnapshot,
2026 window: &mut Window,
2027 cx: &mut App,
2028 ) -> Vec<AnyElement> {
2029 self.editor.update(cx, |editor, cx| {
2030 let active_task_indicator_row =
2031 if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
2032 deployed_from_indicator,
2033 actions,
2034 ..
2035 })) = editor.context_menu.borrow().as_ref()
2036 {
2037 actions
2038 .tasks
2039 .as_ref()
2040 .map(|tasks| tasks.position.to_display_point(snapshot).row())
2041 .or(*deployed_from_indicator)
2042 } else {
2043 None
2044 };
2045
2046 let offset_range_start =
2047 snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left);
2048
2049 let offset_range_end =
2050 snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
2051
2052 editor
2053 .tasks
2054 .iter()
2055 .filter_map(|(_, tasks)| {
2056 let multibuffer_point = tasks.offset.to_point(&snapshot.buffer_snapshot);
2057 if multibuffer_point < offset_range_start
2058 || multibuffer_point > offset_range_end
2059 {
2060 return None;
2061 }
2062 let multibuffer_row = MultiBufferRow(multibuffer_point.row);
2063 let buffer_folded = snapshot
2064 .buffer_snapshot
2065 .buffer_line_for_row(multibuffer_row)
2066 .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
2067 .map(|buffer_id| editor.is_buffer_folded(buffer_id, cx))
2068 .unwrap_or(false);
2069 if buffer_folded {
2070 return None;
2071 }
2072
2073 if snapshot.is_line_folded(multibuffer_row) {
2074 // Skip folded indicators, unless it's the starting line of a fold.
2075 if multibuffer_row
2076 .0
2077 .checked_sub(1)
2078 .map_or(false, |previous_row| {
2079 snapshot.is_line_folded(MultiBufferRow(previous_row))
2080 })
2081 {
2082 return None;
2083 }
2084 }
2085 let display_row = multibuffer_point.to_display_point(snapshot).row();
2086 let button = editor.render_run_indicator(
2087 &self.style,
2088 Some(display_row) == active_task_indicator_row,
2089 display_row,
2090 cx,
2091 );
2092
2093 let button = prepaint_gutter_button(
2094 button,
2095 display_row,
2096 line_height,
2097 gutter_dimensions,
2098 scroll_pixel_position,
2099 gutter_hitbox,
2100 display_hunks,
2101 window,
2102 cx,
2103 );
2104 Some(button)
2105 })
2106 .collect_vec()
2107 })
2108 }
2109
2110 #[allow(clippy::too_many_arguments)]
2111 fn layout_code_actions_indicator(
2112 &self,
2113 line_height: Pixels,
2114 newest_selection_head: DisplayPoint,
2115 scroll_pixel_position: gpui::Point<Pixels>,
2116 gutter_dimensions: &GutterDimensions,
2117 gutter_hitbox: &Hitbox,
2118 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
2119 window: &mut Window,
2120 cx: &mut App,
2121 ) -> Option<AnyElement> {
2122 let mut active = false;
2123 let mut button = None;
2124 let row = newest_selection_head.row();
2125 self.editor.update(cx, |editor, cx| {
2126 if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
2127 deployed_from_indicator,
2128 ..
2129 })) = editor.context_menu.borrow().as_ref()
2130 {
2131 active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
2132 };
2133 button = editor.render_code_actions_indicator(&self.style, row, active, cx);
2134 });
2135
2136 let button = prepaint_gutter_button(
2137 button?,
2138 row,
2139 line_height,
2140 gutter_dimensions,
2141 scroll_pixel_position,
2142 gutter_hitbox,
2143 display_hunks,
2144 window,
2145 cx,
2146 );
2147
2148 Some(button)
2149 }
2150
2151 fn get_participant_color(participant_index: Option<ParticipantIndex>, cx: &App) -> PlayerColor {
2152 if let Some(index) = participant_index {
2153 cx.theme().players().color_for_participant(index.0)
2154 } else {
2155 cx.theme().players().absent()
2156 }
2157 }
2158
2159 fn calculate_relative_line_numbers(
2160 &self,
2161 snapshot: &EditorSnapshot,
2162 rows: &Range<DisplayRow>,
2163 relative_to: Option<DisplayRow>,
2164 ) -> HashMap<DisplayRow, DisplayRowDelta> {
2165 let mut relative_rows: HashMap<DisplayRow, DisplayRowDelta> = Default::default();
2166 let Some(relative_to) = relative_to else {
2167 return relative_rows;
2168 };
2169
2170 let start = rows.start.min(relative_to);
2171 let end = rows.end.max(relative_to);
2172
2173 let buffer_rows = snapshot
2174 .row_infos(start)
2175 .take(1 + end.minus(start) as usize)
2176 .collect::<Vec<_>>();
2177
2178 let head_idx = relative_to.minus(start);
2179 let mut delta = 1;
2180 let mut i = head_idx + 1;
2181 while i < buffer_rows.len() as u32 {
2182 if buffer_rows[i as usize].buffer_row.is_some() {
2183 if rows.contains(&DisplayRow(i + start.0)) {
2184 relative_rows.insert(DisplayRow(i + start.0), delta);
2185 }
2186 delta += 1;
2187 }
2188 i += 1;
2189 }
2190 delta = 1;
2191 i = head_idx.min(buffer_rows.len() as u32 - 1);
2192 while i > 0 && buffer_rows[i as usize].buffer_row.is_none() {
2193 i -= 1;
2194 }
2195
2196 while i > 0 {
2197 i -= 1;
2198 if buffer_rows[i as usize].buffer_row.is_some() {
2199 if rows.contains(&DisplayRow(i + start.0)) {
2200 relative_rows.insert(DisplayRow(i + start.0), delta);
2201 }
2202 delta += 1;
2203 }
2204 }
2205
2206 relative_rows
2207 }
2208
2209 #[allow(clippy::too_many_arguments)]
2210 fn layout_line_numbers(
2211 &self,
2212 gutter_hitbox: Option<&Hitbox>,
2213 gutter_dimensions: GutterDimensions,
2214 line_height: Pixels,
2215 scroll_position: gpui::Point<f32>,
2216 rows: Range<DisplayRow>,
2217 buffer_rows: &[RowInfo],
2218 newest_selection_head: Option<DisplayPoint>,
2219 snapshot: &EditorSnapshot,
2220 window: &mut Window,
2221 cx: &mut App,
2222 ) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> {
2223 let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
2224 EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full
2225 });
2226 if !include_line_numbers {
2227 return Arc::default();
2228 }
2229
2230 let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| {
2231 let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
2232 let newest = editor.selections.newest::<Point>(cx);
2233 SelectionLayout::new(
2234 newest,
2235 editor.selections.line_mode,
2236 editor.cursor_shape,
2237 &snapshot.display_snapshot,
2238 true,
2239 true,
2240 None,
2241 )
2242 .head
2243 });
2244 let is_relative = editor.should_use_relative_line_numbers(cx);
2245 (newest_selection_head, is_relative)
2246 });
2247
2248 let relative_to = if is_relative {
2249 Some(newest_selection_head.row())
2250 } else {
2251 None
2252 };
2253 let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
2254 let mut line_number = String::new();
2255 let line_numbers = buffer_rows
2256 .into_iter()
2257 .enumerate()
2258 .flat_map(|(ix, row_info)| {
2259 let display_row = DisplayRow(rows.start.0 + ix as u32);
2260 line_number.clear();
2261 let non_relative_number = row_info.buffer_row? + 1;
2262 let number = relative_rows
2263 .get(&display_row)
2264 .unwrap_or(&non_relative_number);
2265 write!(&mut line_number, "{number}").unwrap();
2266 if row_info
2267 .diff_status
2268 .is_some_and(|status| status.is_deleted())
2269 {
2270 return None;
2271 }
2272
2273 let color = cx.theme().colors().editor_line_number;
2274 let shaped_line = self
2275 .shape_line_number(SharedString::from(&line_number), color, window)
2276 .log_err()?;
2277 let scroll_top = scroll_position.y * line_height;
2278 let line_origin = gutter_hitbox.map(|hitbox| {
2279 hitbox.origin
2280 + point(
2281 hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
2282 ix as f32 * line_height - (scroll_top % line_height),
2283 )
2284 });
2285
2286 #[cfg(not(test))]
2287 let hitbox = line_origin.map(|line_origin| {
2288 window.insert_hitbox(
2289 Bounds::new(line_origin, size(shaped_line.width, line_height)),
2290 false,
2291 )
2292 });
2293 #[cfg(test)]
2294 let hitbox = {
2295 let _ = line_origin;
2296 None
2297 };
2298
2299 let multi_buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
2300 let multi_buffer_row = MultiBufferRow(multi_buffer_row);
2301 let line_number = LineNumberLayout {
2302 shaped_line,
2303 hitbox,
2304 display_row,
2305 };
2306 Some((multi_buffer_row, line_number))
2307 })
2308 .collect();
2309 Arc::new(line_numbers)
2310 }
2311
2312 fn layout_crease_toggles(
2313 &self,
2314 rows: Range<DisplayRow>,
2315 row_infos: &[RowInfo],
2316 active_rows: &BTreeMap<DisplayRow, bool>,
2317 snapshot: &EditorSnapshot,
2318 window: &mut Window,
2319 cx: &mut App,
2320 ) -> Vec<Option<AnyElement>> {
2321 let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
2322 && snapshot.mode == EditorMode::Full
2323 && self.editor.read(cx).is_singleton(cx);
2324 if include_fold_statuses {
2325 row_infos
2326 .into_iter()
2327 .enumerate()
2328 .map(|(ix, info)| {
2329 let row = info.multibuffer_row?;
2330 let display_row = DisplayRow(rows.start.0 + ix as u32);
2331 let active = active_rows.contains_key(&display_row);
2332
2333 snapshot.render_crease_toggle(row, active, self.editor.clone(), window, cx)
2334 })
2335 .collect()
2336 } else {
2337 Vec::new()
2338 }
2339 }
2340
2341 fn layout_crease_trailers(
2342 &self,
2343 buffer_rows: impl IntoIterator<Item = RowInfo>,
2344 snapshot: &EditorSnapshot,
2345 window: &mut Window,
2346 cx: &mut App,
2347 ) -> Vec<Option<AnyElement>> {
2348 buffer_rows
2349 .into_iter()
2350 .map(|row_info| {
2351 if let Some(row) = row_info.multibuffer_row {
2352 snapshot.render_crease_trailer(row, window, cx)
2353 } else {
2354 None
2355 }
2356 })
2357 .collect()
2358 }
2359
2360 fn layout_lines(
2361 rows: Range<DisplayRow>,
2362 snapshot: &EditorSnapshot,
2363 style: &EditorStyle,
2364 editor_width: Pixels,
2365 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
2366 window: &mut Window,
2367 cx: &mut App,
2368 ) -> Vec<LineWithInvisibles> {
2369 if rows.start >= rows.end {
2370 return Vec::new();
2371 }
2372
2373 // Show the placeholder when the editor is empty
2374 if snapshot.is_empty() {
2375 let font_size = style.text.font_size.to_pixels(window.rem_size());
2376 let placeholder_color = cx.theme().colors().text_placeholder;
2377 let placeholder_text = snapshot.placeholder_text();
2378
2379 let placeholder_lines = placeholder_text
2380 .as_ref()
2381 .map_or("", AsRef::as_ref)
2382 .split('\n')
2383 .skip(rows.start.0 as usize)
2384 .chain(iter::repeat(""))
2385 .take(rows.len());
2386 placeholder_lines
2387 .filter_map(move |line| {
2388 let run = TextRun {
2389 len: line.len(),
2390 font: style.text.font(),
2391 color: placeholder_color,
2392 background_color: None,
2393 underline: Default::default(),
2394 strikethrough: None,
2395 };
2396 window
2397 .text_system()
2398 .shape_line(line.to_string().into(), font_size, &[run])
2399 .log_err()
2400 })
2401 .map(|line| LineWithInvisibles {
2402 width: line.width,
2403 len: line.len,
2404 fragments: smallvec![LineFragment::Text(line)],
2405 invisibles: Vec::new(),
2406 font_size,
2407 })
2408 .collect()
2409 } else {
2410 let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
2411 LineWithInvisibles::from_chunks(
2412 chunks,
2413 &style,
2414 MAX_LINE_LEN,
2415 rows.len(),
2416 snapshot.mode,
2417 editor_width,
2418 is_row_soft_wrapped,
2419 window,
2420 cx,
2421 )
2422 }
2423 }
2424
2425 #[allow(clippy::too_many_arguments)]
2426 fn prepaint_lines(
2427 &self,
2428 start_row: DisplayRow,
2429 line_layouts: &mut [LineWithInvisibles],
2430 line_height: Pixels,
2431 scroll_pixel_position: gpui::Point<Pixels>,
2432 content_origin: gpui::Point<Pixels>,
2433 window: &mut Window,
2434 cx: &mut App,
2435 ) -> SmallVec<[AnyElement; 1]> {
2436 let mut line_elements = SmallVec::new();
2437 for (ix, line) in line_layouts.iter_mut().enumerate() {
2438 let row = start_row + DisplayRow(ix as u32);
2439 line.prepaint(
2440 line_height,
2441 scroll_pixel_position,
2442 row,
2443 content_origin,
2444 &mut line_elements,
2445 window,
2446 cx,
2447 );
2448 }
2449 line_elements
2450 }
2451
2452 #[allow(clippy::too_many_arguments)]
2453 fn render_block(
2454 &self,
2455 block: &Block,
2456 available_width: AvailableSpace,
2457 block_id: BlockId,
2458 block_row_start: DisplayRow,
2459 snapshot: &EditorSnapshot,
2460 text_x: Pixels,
2461 rows: &Range<DisplayRow>,
2462 line_layouts: &[LineWithInvisibles],
2463 gutter_dimensions: &GutterDimensions,
2464 line_height: Pixels,
2465 em_width: Pixels,
2466 text_hitbox: &Hitbox,
2467 editor_width: Pixels,
2468 scroll_width: &mut Pixels,
2469 resized_blocks: &mut HashMap<CustomBlockId, u32>,
2470 selections: &[Selection<Point>],
2471 selected_buffer_ids: &Vec<BufferId>,
2472 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
2473 sticky_header_excerpt_id: Option<ExcerptId>,
2474 window: &mut Window,
2475 cx: &mut App,
2476 ) -> (AnyElement, Size<Pixels>) {
2477 let mut element = match block {
2478 Block::Custom(block) => {
2479 let block_start = block.start().to_point(&snapshot.buffer_snapshot);
2480 let block_end = block.end().to_point(&snapshot.buffer_snapshot);
2481 let align_to = block_start.to_display_point(snapshot);
2482 let anchor_x = text_x
2483 + if rows.contains(&align_to.row()) {
2484 line_layouts[align_to.row().minus(rows.start) as usize]
2485 .x_for_index(align_to.column() as usize)
2486 } else {
2487 layout_line(
2488 align_to.row(),
2489 snapshot,
2490 &self.style,
2491 editor_width,
2492 is_row_soft_wrapped,
2493 window,
2494 cx,
2495 )
2496 .x_for_index(align_to.column() as usize)
2497 };
2498
2499 let selected = selections
2500 .binary_search_by(|selection| {
2501 if selection.end <= block_start {
2502 Ordering::Less
2503 } else if selection.start >= block_end {
2504 Ordering::Greater
2505 } else {
2506 Ordering::Equal
2507 }
2508 })
2509 .is_ok();
2510
2511 div()
2512 .size_full()
2513 .child(block.render(&mut BlockContext {
2514 window,
2515 app: cx,
2516 anchor_x,
2517 gutter_dimensions,
2518 line_height,
2519 em_width,
2520 block_id,
2521 selected,
2522 max_width: text_hitbox.size.width.max(*scroll_width),
2523 editor_style: &self.style,
2524 }))
2525 .into_any()
2526 }
2527
2528 Block::FoldedBuffer {
2529 first_excerpt,
2530 prev_excerpt,
2531 show_excerpt_controls,
2532 height,
2533 } => {
2534 let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
2535 let mut result = v_flex().id(block_id).w_full();
2536
2537 if let Some(prev_excerpt) = prev_excerpt {
2538 if *show_excerpt_controls {
2539 result = result.child(self.render_expand_excerpt_control(
2540 block_id,
2541 ExpandExcerptDirection::Down,
2542 prev_excerpt.id,
2543 gutter_dimensions,
2544 window,
2545 cx,
2546 ));
2547 }
2548 }
2549
2550 let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt);
2551 result
2552 .child(self.render_buffer_header(
2553 first_excerpt,
2554 true,
2555 selected,
2556 jump_data,
2557 window,
2558 cx,
2559 ))
2560 .into_any_element()
2561 }
2562
2563 Block::ExcerptBoundary {
2564 prev_excerpt,
2565 next_excerpt,
2566 show_excerpt_controls,
2567 height,
2568 starts_new_buffer,
2569 } => {
2570 let color = cx.theme().colors().clone();
2571 let mut result = v_flex().id(block_id).w_full();
2572
2573 if let Some(prev_excerpt) = prev_excerpt {
2574 if *show_excerpt_controls {
2575 result = result.child(self.render_expand_excerpt_control(
2576 block_id,
2577 ExpandExcerptDirection::Down,
2578 prev_excerpt.id,
2579 gutter_dimensions,
2580 window,
2581 cx,
2582 ));
2583 }
2584 }
2585
2586 if let Some(next_excerpt) = next_excerpt {
2587 let jump_data =
2588 header_jump_data(snapshot, block_row_start, *height, next_excerpt);
2589
2590 if *starts_new_buffer {
2591 if sticky_header_excerpt_id != Some(next_excerpt.id) {
2592 let selected = selected_buffer_ids.contains(&next_excerpt.buffer_id);
2593
2594 result = result.child(self.render_buffer_header(
2595 next_excerpt,
2596 false,
2597 selected,
2598 jump_data,
2599 window,
2600 cx,
2601 ));
2602 } else {
2603 result = result
2604 .child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
2605 }
2606
2607 if *show_excerpt_controls {
2608 result = result.child(self.render_expand_excerpt_control(
2609 block_id,
2610 ExpandExcerptDirection::Up,
2611 next_excerpt.id,
2612 gutter_dimensions,
2613 window,
2614 cx,
2615 ));
2616 }
2617 } else {
2618 if *show_excerpt_controls {
2619 result = result.child(
2620 h_flex()
2621 .relative()
2622 .child(
2623 div()
2624 .top(px(0.))
2625 .absolute()
2626 .w_full()
2627 .h_px()
2628 .bg(color.border_variant),
2629 )
2630 .child(self.render_expand_excerpt_control(
2631 block_id,
2632 ExpandExcerptDirection::Up,
2633 next_excerpt.id,
2634 gutter_dimensions,
2635 window,
2636 cx,
2637 )),
2638 );
2639 }
2640 };
2641 }
2642
2643 result.into_any()
2644 }
2645 };
2646
2647 // Discover the element's content height, then round up to the nearest multiple of line height.
2648 let preliminary_size = element.layout_as_root(
2649 size(available_width, AvailableSpace::MinContent),
2650 window,
2651 cx,
2652 );
2653 let quantized_height = (preliminary_size.height / line_height).ceil() * line_height;
2654 let final_size = if preliminary_size.height == quantized_height {
2655 preliminary_size
2656 } else {
2657 element.layout_as_root(size(available_width, quantized_height.into()), window, cx)
2658 };
2659
2660 if let BlockId::Custom(custom_block_id) = block_id {
2661 if block.height() > 0 {
2662 let element_height_in_lines =
2663 ((final_size.height / line_height).ceil() as u32).max(1);
2664 if element_height_in_lines != block.height() {
2665 resized_blocks.insert(custom_block_id, element_height_in_lines);
2666 }
2667 }
2668 }
2669
2670 (element, final_size)
2671 }
2672
2673 fn render_buffer_header(
2674 &self,
2675 for_excerpt: &ExcerptInfo,
2676 is_folded: bool,
2677 is_selected: bool,
2678 jump_data: JumpData,
2679 window: &mut Window,
2680 cx: &mut App,
2681 ) -> Div {
2682 let editor = self.editor.read(cx);
2683 let file_status = editor
2684 .buffer
2685 .read(cx)
2686 .all_diff_hunks_expanded()
2687 .then(|| {
2688 editor
2689 .project
2690 .as_ref()?
2691 .read(cx)
2692 .status_for_buffer_id(for_excerpt.buffer_id, cx)
2693 })
2694 .flatten();
2695
2696 let include_root = editor
2697 .project
2698 .as_ref()
2699 .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
2700 .unwrap_or_default();
2701 let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
2702 let filename = path
2703 .as_ref()
2704 .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
2705 let parent_path = path.as_ref().and_then(|path| {
2706 Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
2707 });
2708 let focus_handle = editor.focus_handle(cx);
2709 let colors = cx.theme().colors();
2710
2711 div()
2712 .px_2()
2713 .pt_2()
2714 .w_full()
2715 .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
2716 .child(
2717 h_flex()
2718 .size_full()
2719 .gap_2()
2720 .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
2721 .pl_0p5()
2722 .pr_5()
2723 .rounded_md()
2724 .shadow_md()
2725 .border_1()
2726 .map(|div| {
2727 let border_color = if is_selected
2728 && is_folded
2729 && focus_handle.contains_focused(window, cx)
2730 {
2731 colors.border_focused
2732 } else {
2733 colors.border
2734 };
2735 div.border_color(border_color)
2736 })
2737 .bg(colors.editor_subheader_background)
2738 .hover(|style| style.bg(colors.element_hover))
2739 .map(|header| {
2740 let editor = self.editor.clone();
2741 let buffer_id = for_excerpt.buffer_id;
2742 let toggle_chevron_icon =
2743 FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
2744 header.child(
2745 div()
2746 .hover(|style| style.bg(colors.element_selected))
2747 .rounded_sm()
2748 .child(
2749 ButtonLike::new("toggle-buffer-fold")
2750 .style(ui::ButtonStyle::Transparent)
2751 .size(ButtonSize::Large)
2752 .width(px(30.).into())
2753 .children(toggle_chevron_icon)
2754 .tooltip({
2755 let focus_handle = focus_handle.clone();
2756 move |window, cx| {
2757 Tooltip::for_action_in(
2758 "Toggle Excerpt Fold",
2759 &ToggleFold,
2760 &focus_handle,
2761 window,
2762 cx,
2763 )
2764 }
2765 })
2766 .on_click(move |_, _, cx| {
2767 if is_folded {
2768 editor.update(cx, |editor, cx| {
2769 editor.unfold_buffer(buffer_id, cx);
2770 });
2771 } else {
2772 editor.update(cx, |editor, cx| {
2773 editor.fold_buffer(buffer_id, cx);
2774 });
2775 }
2776 }),
2777 ),
2778 )
2779 })
2780 .children(
2781 editor
2782 .addons
2783 .values()
2784 .filter_map(|addon| {
2785 addon.render_buffer_header_controls(for_excerpt, window, cx)
2786 })
2787 .take(1),
2788 )
2789 .child(
2790 h_flex()
2791 .cursor_pointer()
2792 .id("path header block")
2793 .size_full()
2794 .justify_between()
2795 .child(
2796 h_flex()
2797 .gap_2()
2798 .child(
2799 Label::new(
2800 filename
2801 .map(SharedString::from)
2802 .unwrap_or_else(|| "untitled".into()),
2803 )
2804 .single_line()
2805 .when_some(
2806 file_status,
2807 |el, status| {
2808 el.color(if status.is_conflicted() {
2809 Color::Conflict
2810 } else if status.is_modified() {
2811 Color::Modified
2812 } else if status.is_deleted() {
2813 Color::Disabled
2814 } else {
2815 Color::Created
2816 })
2817 .when(status.is_deleted(), |el| el.strikethrough())
2818 },
2819 ),
2820 )
2821 .when_some(parent_path, |then, path| {
2822 then.child(div().child(path).text_color(
2823 if file_status.is_some_and(FileStatus::is_deleted) {
2824 colors.text_disabled
2825 } else {
2826 colors.text_muted
2827 },
2828 ))
2829 }),
2830 )
2831 .when(is_selected, |el| {
2832 el.child(
2833 h_flex()
2834 .id("jump-to-file-button")
2835 .gap_2p5()
2836 .child(Label::new("Jump To File"))
2837 .children(
2838 KeyBinding::for_action_in(
2839 &OpenExcerpts,
2840 &focus_handle,
2841 window,
2842 cx,
2843 )
2844 .map(|binding| binding.into_any_element()),
2845 ),
2846 )
2847 })
2848 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
2849 .on_click(window.listener_for(&self.editor, {
2850 move |editor, e: &ClickEvent, window, cx| {
2851 editor.open_excerpts_common(
2852 Some(jump_data.clone()),
2853 e.down.modifiers.secondary(),
2854 window,
2855 cx,
2856 );
2857 }
2858 })),
2859 ),
2860 )
2861 }
2862
2863 fn render_expand_excerpt_control(
2864 &self,
2865 block_id: BlockId,
2866 direction: ExpandExcerptDirection,
2867 excerpt_id: ExcerptId,
2868 gutter_dimensions: &GutterDimensions,
2869 window: &Window,
2870 cx: &mut App,
2871 ) -> impl IntoElement {
2872 let color = cx.theme().colors().clone();
2873 let hover_color = color.border_variant.opacity(0.5);
2874 let focus_handle = self.editor.focus_handle(cx).clone();
2875
2876 let icon_offset =
2877 gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin);
2878 let header_height = MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * window.line_height();
2879 let group_name = if direction == ExpandExcerptDirection::Down {
2880 "expand-down"
2881 } else {
2882 "expand-up"
2883 };
2884
2885 let expand_area = |id: SharedString| {
2886 h_flex()
2887 .id(id)
2888 .w_full()
2889 .cursor_pointer()
2890 .block_mouse_down()
2891 .on_mouse_move(|_, _, cx| cx.stop_propagation())
2892 .hover(|style| style.bg(hover_color))
2893 .tooltip({
2894 let focus_handle = focus_handle.clone();
2895 move |window, cx| {
2896 Tooltip::for_action_in(
2897 "Expand Excerpt",
2898 &ExpandExcerpts { lines: 0 },
2899 &focus_handle,
2900 window,
2901 cx,
2902 )
2903 }
2904 })
2905 };
2906
2907 expand_area(
2908 format!(
2909 "block-{}-{}",
2910 block_id,
2911 if direction == ExpandExcerptDirection::Down {
2912 "down"
2913 } else {
2914 "up"
2915 }
2916 )
2917 .into(),
2918 )
2919 .group(group_name)
2920 .child(
2921 h_flex()
2922 .w(icon_offset)
2923 .h(header_height)
2924 .flex_none()
2925 .justify_end()
2926 .child(
2927 ButtonLike::new("expand-icon")
2928 .style(ButtonStyle::Transparent)
2929 .child(
2930 svg()
2931 .path(if direction == ExpandExcerptDirection::Down {
2932 IconName::ArrowDownFromLine.path()
2933 } else {
2934 IconName::ArrowUpFromLine.path()
2935 })
2936 .size(IconSize::XSmall.rems())
2937 .text_color(cx.theme().colors().editor_line_number)
2938 .group_hover(group_name, |style| {
2939 style.text_color(cx.theme().colors().editor_active_line_number)
2940 }),
2941 ),
2942 ),
2943 )
2944 .on_click(window.listener_for(&self.editor, {
2945 move |editor, _, _, cx| {
2946 editor.expand_excerpt(excerpt_id, direction, cx);
2947 cx.stop_propagation();
2948 }
2949 }))
2950 }
2951
2952 #[allow(clippy::too_many_arguments)]
2953 fn render_blocks(
2954 &self,
2955 rows: Range<DisplayRow>,
2956 snapshot: &EditorSnapshot,
2957 hitbox: &Hitbox,
2958 text_hitbox: &Hitbox,
2959 editor_width: Pixels,
2960 scroll_width: &mut Pixels,
2961 gutter_dimensions: &GutterDimensions,
2962 em_width: Pixels,
2963 text_x: Pixels,
2964 line_height: Pixels,
2965 line_layouts: &[LineWithInvisibles],
2966 selections: &[Selection<Point>],
2967 selected_buffer_ids: &Vec<BufferId>,
2968 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
2969 sticky_header_excerpt_id: Option<ExcerptId>,
2970 window: &mut Window,
2971 cx: &mut App,
2972 ) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
2973 let (fixed_blocks, non_fixed_blocks) = snapshot
2974 .blocks_in_range(rows.clone())
2975 .partition::<Vec<_>, _>(|(_, block)| block.style() == BlockStyle::Fixed);
2976
2977 let mut focused_block = self
2978 .editor
2979 .update(cx, |editor, _| editor.take_focused_block());
2980 let mut fixed_block_max_width = Pixels::ZERO;
2981 let mut blocks = Vec::new();
2982 let mut resized_blocks = HashMap::default();
2983
2984 for (row, block) in fixed_blocks {
2985 let block_id = block.id();
2986
2987 if focused_block.as_ref().map_or(false, |b| b.id == block_id) {
2988 focused_block = None;
2989 }
2990
2991 let (element, element_size) = self.render_block(
2992 block,
2993 AvailableSpace::MinContent,
2994 block_id,
2995 row,
2996 snapshot,
2997 text_x,
2998 &rows,
2999 line_layouts,
3000 gutter_dimensions,
3001 line_height,
3002 em_width,
3003 text_hitbox,
3004 editor_width,
3005 scroll_width,
3006 &mut resized_blocks,
3007 selections,
3008 selected_buffer_ids,
3009 is_row_soft_wrapped,
3010 sticky_header_excerpt_id,
3011 window,
3012 cx,
3013 );
3014 fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
3015 blocks.push(BlockLayout {
3016 id: block_id,
3017 row: Some(row),
3018 element,
3019 available_space: size(AvailableSpace::MinContent, element_size.height.into()),
3020 style: BlockStyle::Fixed,
3021 });
3022 }
3023
3024 for (row, block) in non_fixed_blocks {
3025 let style = block.style();
3026 let width = match style {
3027 BlockStyle::Sticky => hitbox.size.width,
3028 BlockStyle::Flex => hitbox
3029 .size
3030 .width
3031 .max(fixed_block_max_width)
3032 .max(gutter_dimensions.width + *scroll_width),
3033 BlockStyle::Fixed => unreachable!(),
3034 };
3035 let block_id = block.id();
3036
3037 if focused_block.as_ref().map_or(false, |b| b.id == block_id) {
3038 focused_block = None;
3039 }
3040
3041 let (element, element_size) = self.render_block(
3042 block,
3043 width.into(),
3044 block_id,
3045 row,
3046 snapshot,
3047 text_x,
3048 &rows,
3049 line_layouts,
3050 gutter_dimensions,
3051 line_height,
3052 em_width,
3053 text_hitbox,
3054 editor_width,
3055 scroll_width,
3056 &mut resized_blocks,
3057 selections,
3058 selected_buffer_ids,
3059 is_row_soft_wrapped,
3060 sticky_header_excerpt_id,
3061 window,
3062 cx,
3063 );
3064
3065 blocks.push(BlockLayout {
3066 id: block_id,
3067 row: Some(row),
3068 element,
3069 available_space: size(width.into(), element_size.height.into()),
3070 style,
3071 });
3072 }
3073
3074 if let Some(focused_block) = focused_block {
3075 if let Some(focus_handle) = focused_block.focus_handle.upgrade() {
3076 if focus_handle.is_focused(window) {
3077 if let Some(block) = snapshot.block_for_id(focused_block.id) {
3078 let style = block.style();
3079 let width = match style {
3080 BlockStyle::Fixed => AvailableSpace::MinContent,
3081 BlockStyle::Flex => AvailableSpace::Definite(
3082 hitbox
3083 .size
3084 .width
3085 .max(fixed_block_max_width)
3086 .max(gutter_dimensions.width + *scroll_width),
3087 ),
3088 BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
3089 };
3090
3091 let (element, element_size) = self.render_block(
3092 &block,
3093 width,
3094 focused_block.id,
3095 rows.end,
3096 snapshot,
3097 text_x,
3098 &rows,
3099 line_layouts,
3100 gutter_dimensions,
3101 line_height,
3102 em_width,
3103 text_hitbox,
3104 editor_width,
3105 scroll_width,
3106 &mut resized_blocks,
3107 selections,
3108 selected_buffer_ids,
3109 is_row_soft_wrapped,
3110 sticky_header_excerpt_id,
3111 window,
3112 cx,
3113 );
3114
3115 blocks.push(BlockLayout {
3116 id: block.id(),
3117 row: None,
3118 element,
3119 available_space: size(width, element_size.height.into()),
3120 style,
3121 });
3122 }
3123 }
3124 }
3125 }
3126
3127 if resized_blocks.is_empty() {
3128 *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width);
3129 Ok(blocks)
3130 } else {
3131 Err(resized_blocks)
3132 }
3133 }
3134
3135 /// Returns true if any of the blocks changed size since the previous frame. This will trigger
3136 /// a restart of rendering for the editor based on the new sizes.
3137 #[allow(clippy::too_many_arguments)]
3138 fn layout_blocks(
3139 &self,
3140 blocks: &mut Vec<BlockLayout>,
3141 block_starts: &mut HashSet<DisplayRow>,
3142 hitbox: &Hitbox,
3143 line_height: Pixels,
3144 scroll_pixel_position: gpui::Point<Pixels>,
3145 window: &mut Window,
3146 cx: &mut App,
3147 ) {
3148 for block in blocks {
3149 let mut origin = if let Some(row) = block.row {
3150 block_starts.insert(row);
3151 hitbox.origin
3152 + point(
3153 Pixels::ZERO,
3154 row.as_f32() * line_height - scroll_pixel_position.y,
3155 )
3156 } else {
3157 // Position the block outside the visible area
3158 hitbox.origin + point(Pixels::ZERO, hitbox.size.height)
3159 };
3160
3161 if !matches!(block.style, BlockStyle::Sticky) {
3162 origin += point(-scroll_pixel_position.x, Pixels::ZERO);
3163 }
3164
3165 let focus_handle =
3166 block
3167 .element
3168 .prepaint_as_root(origin, block.available_space, window, cx);
3169
3170 if let Some(focus_handle) = focus_handle {
3171 self.editor.update(cx, |editor, _cx| {
3172 editor.set_focused_block(FocusedBlock {
3173 id: block.id,
3174 focus_handle: focus_handle.downgrade(),
3175 });
3176 });
3177 }
3178 }
3179 }
3180
3181 #[allow(clippy::too_many_arguments)]
3182 fn layout_sticky_buffer_header(
3183 &self,
3184 StickyHeaderExcerpt {
3185 excerpt,
3186 next_excerpt_controls_present,
3187 next_buffer_row,
3188 }: StickyHeaderExcerpt<'_>,
3189 scroll_position: f32,
3190 line_height: Pixels,
3191 snapshot: &EditorSnapshot,
3192 hitbox: &Hitbox,
3193 selected_buffer_ids: &Vec<BufferId>,
3194 window: &mut Window,
3195 cx: &mut App,
3196 ) -> AnyElement {
3197 let jump_data = header_jump_data(
3198 snapshot,
3199 DisplayRow(scroll_position as u32),
3200 FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
3201 excerpt,
3202 );
3203
3204 let editor_bg_color = cx.theme().colors().editor_background;
3205
3206 let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
3207
3208 let mut header = v_flex()
3209 .relative()
3210 .child(
3211 div()
3212 .w(hitbox.bounds.size.width)
3213 .h(FILE_HEADER_HEIGHT as f32 * line_height)
3214 .bg(linear_gradient(
3215 0.,
3216 linear_color_stop(editor_bg_color.opacity(0.), 0.),
3217 linear_color_stop(editor_bg_color, 0.6),
3218 ))
3219 .absolute()
3220 .top_0(),
3221 )
3222 .child(
3223 self.render_buffer_header(excerpt, false, selected, jump_data, window, cx)
3224 .into_any_element(),
3225 )
3226 .into_any_element();
3227
3228 let mut origin = hitbox.origin;
3229
3230 if let Some(next_buffer_row) = next_buffer_row {
3231 // Push up the sticky header when the excerpt is getting close to the top of the viewport
3232
3233 let mut max_row = next_buffer_row - FILE_HEADER_HEIGHT * 2;
3234
3235 if next_excerpt_controls_present {
3236 max_row -= MULTI_BUFFER_EXCERPT_HEADER_HEIGHT;
3237 }
3238
3239 let offset = scroll_position - max_row as f32;
3240
3241 if offset > 0.0 {
3242 origin.y -= Pixels(offset) * line_height;
3243 }
3244 }
3245
3246 let size = size(
3247 AvailableSpace::Definite(hitbox.size.width),
3248 AvailableSpace::MinContent,
3249 );
3250
3251 header.prepaint_as_root(origin, size, window, cx);
3252
3253 header
3254 }
3255
3256 #[allow(clippy::too_many_arguments)]
3257 fn layout_cursor_popovers(
3258 &self,
3259 line_height: Pixels,
3260 text_hitbox: &Hitbox,
3261 content_origin: gpui::Point<Pixels>,
3262 start_row: DisplayRow,
3263 scroll_pixel_position: gpui::Point<Pixels>,
3264 line_layouts: &[LineWithInvisibles],
3265 cursor: DisplayPoint,
3266 cursor_point: Point,
3267 style: &EditorStyle,
3268 window: &mut Window,
3269 cx: &mut App,
3270 ) {
3271 let mut min_menu_height = Pixels::ZERO;
3272 let mut max_menu_height = Pixels::ZERO;
3273 let mut height_above_menu = Pixels::ZERO;
3274 let height_below_menu = Pixels::ZERO;
3275 let mut edit_prediction_popover_visible = false;
3276 let mut context_menu_visible = false;
3277
3278 {
3279 let editor = self.editor.read(cx);
3280 if editor
3281 .edit_prediction_visible_in_cursor_popover(editor.has_active_inline_completion())
3282 {
3283 height_above_menu +=
3284 editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING;
3285 edit_prediction_popover_visible = true;
3286 }
3287
3288 if editor.context_menu_visible() {
3289 if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() {
3290 min_menu_height += line_height * 3. + POPOVER_Y_PADDING;
3291 max_menu_height += line_height * 12. + POPOVER_Y_PADDING;
3292 context_menu_visible = true;
3293 }
3294 }
3295 }
3296
3297 let visible = edit_prediction_popover_visible || context_menu_visible;
3298 if !visible {
3299 return;
3300 }
3301
3302 let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
3303 let target_position = content_origin
3304 + gpui::Point {
3305 x: cmp::max(
3306 px(0.),
3307 cursor_row_layout.x_for_index(cursor.column() as usize)
3308 - scroll_pixel_position.x,
3309 ),
3310 y: cmp::max(
3311 px(0.),
3312 cursor.row().next_row().as_f32() * line_height - scroll_pixel_position.y,
3313 ),
3314 };
3315
3316 let viewport_bounds =
3317 Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
3318 right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
3319 ..Default::default()
3320 });
3321
3322 let min_height = height_above_menu + min_menu_height + height_below_menu;
3323 let max_height = height_above_menu + max_menu_height + height_below_menu;
3324 let Some((laid_out_popovers, y_flipped)) = self.layout_popovers_above_or_below_line(
3325 target_position,
3326 line_height,
3327 min_height,
3328 max_height,
3329 text_hitbox,
3330 viewport_bounds,
3331 window,
3332 cx,
3333 |height, max_width_for_stable_x, y_flipped, window, cx| {
3334 // First layout the menu to get its size - others can be at least this wide.
3335 let context_menu = if context_menu_visible {
3336 let menu_height = if y_flipped {
3337 height - height_below_menu
3338 } else {
3339 height - height_above_menu
3340 };
3341 let mut element = self
3342 .render_context_menu(line_height, menu_height, y_flipped, window, cx)
3343 .expect("Visible context menu should always render.");
3344 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3345 Some((CursorPopoverType::CodeContextMenu, element, size))
3346 } else {
3347 None
3348 };
3349 let min_width = context_menu
3350 .as_ref()
3351 .map_or(px(0.), |(_, _, size)| size.width);
3352 let max_width = max_width_for_stable_x.max(
3353 context_menu
3354 .as_ref()
3355 .map_or(px(0.), |(_, _, size)| size.width),
3356 );
3357
3358 let edit_prediction = if edit_prediction_popover_visible {
3359 self.editor.update(cx, move |editor, cx| {
3360 let accept_binding = editor.accept_edit_prediction_keybind(window, cx);
3361 let mut element = editor.render_edit_prediction_cursor_popover(
3362 min_width,
3363 max_width,
3364 cursor_point,
3365 style,
3366 accept_binding.keystroke(),
3367 window,
3368 cx,
3369 )?;
3370 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3371 Some((CursorPopoverType::EditPrediction, element, size))
3372 })
3373 } else {
3374 None
3375 };
3376 vec![edit_prediction, context_menu]
3377 .into_iter()
3378 .flatten()
3379 .collect::<Vec<_>>()
3380 },
3381 ) else {
3382 return;
3383 };
3384
3385 let Some((menu_ix, (_, menu_bounds))) = laid_out_popovers
3386 .iter()
3387 .find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))
3388 else {
3389 return;
3390 };
3391 let last_ix = laid_out_popovers.len() - 1;
3392 let menu_is_last = menu_ix == last_ix;
3393 let first_popover_bounds = laid_out_popovers[0].1;
3394 let last_popover_bounds = laid_out_popovers[last_ix].1;
3395
3396 // Bounds to layout the aside around. When y_flipped, the aside goes either above or to the
3397 // right, and otherwise it goes below or to the right.
3398 let mut target_bounds = Bounds::from_corners(
3399 first_popover_bounds.origin,
3400 last_popover_bounds.bottom_right(),
3401 );
3402 target_bounds.size.width = menu_bounds.size.width;
3403
3404 // Like `target_bounds`, but with the max height it could occupy. Choosing an aside position
3405 // based on this is preferred for layout stability.
3406 let mut max_target_bounds = target_bounds;
3407 max_target_bounds.size.height = max_height;
3408 if y_flipped {
3409 max_target_bounds.origin.y -= max_height - target_bounds.size.height;
3410 }
3411
3412 // Add spacing around `target_bounds` and `max_target_bounds`.
3413 let mut extend_amount = Edges::all(MENU_GAP);
3414 if y_flipped {
3415 extend_amount.bottom = line_height;
3416 } else {
3417 extend_amount.top = line_height;
3418 }
3419 let target_bounds = target_bounds.extend(extend_amount);
3420 let max_target_bounds = max_target_bounds.extend(extend_amount);
3421
3422 let must_place_above_or_below =
3423 if y_flipped && !menu_is_last && menu_bounds.size.height < max_menu_height {
3424 laid_out_popovers[menu_ix + 1..]
3425 .iter()
3426 .any(|(_, popover_bounds)| popover_bounds.size.width > menu_bounds.size.width)
3427 } else {
3428 false
3429 };
3430
3431 self.layout_context_menu_aside(
3432 y_flipped,
3433 *menu_bounds,
3434 target_bounds,
3435 max_target_bounds,
3436 max_menu_height,
3437 must_place_above_or_below,
3438 text_hitbox,
3439 viewport_bounds,
3440 window,
3441 cx,
3442 );
3443 }
3444
3445 #[allow(clippy::too_many_arguments)]
3446 fn layout_gutter_menu(
3447 &self,
3448 line_height: Pixels,
3449 text_hitbox: &Hitbox,
3450 content_origin: gpui::Point<Pixels>,
3451 scroll_pixel_position: gpui::Point<Pixels>,
3452 gutter_overshoot: Pixels,
3453 window: &mut Window,
3454 cx: &mut App,
3455 ) {
3456 let editor = self.editor.read(cx);
3457 if !editor.context_menu_visible() {
3458 return;
3459 }
3460 let Some(crate::ContextMenuOrigin::GutterIndicator(gutter_row)) =
3461 editor.context_menu_origin()
3462 else {
3463 return;
3464 };
3465 // Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the
3466 // indicator than just a plain first column of the text field.
3467 let target_position = content_origin
3468 + gpui::Point {
3469 x: -gutter_overshoot,
3470 y: gutter_row.next_row().as_f32() * line_height - scroll_pixel_position.y,
3471 };
3472 let min_height = line_height * 3. + POPOVER_Y_PADDING;
3473 let max_height = line_height * 12. + POPOVER_Y_PADDING;
3474 let viewport_bounds =
3475 Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
3476 right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
3477 ..Default::default()
3478 });
3479 self.layout_popovers_above_or_below_line(
3480 target_position,
3481 line_height,
3482 min_height,
3483 max_height,
3484 text_hitbox,
3485 viewport_bounds,
3486 window,
3487 cx,
3488 move |height, _max_width_for_stable_x, y_flipped, window, cx| {
3489 let mut element = self
3490 .render_context_menu(line_height, height, y_flipped, window, cx)
3491 .expect("Visible context menu should always render.");
3492 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
3493 vec![(CursorPopoverType::CodeContextMenu, element, size)]
3494 },
3495 );
3496 }
3497
3498 #[allow(clippy::too_many_arguments)]
3499 fn layout_popovers_above_or_below_line(
3500 &self,
3501 target_position: gpui::Point<Pixels>,
3502 line_height: Pixels,
3503 min_height: Pixels,
3504 max_height: Pixels,
3505 text_hitbox: &Hitbox,
3506 viewport_bounds: Bounds<Pixels>,
3507 window: &mut Window,
3508 cx: &mut App,
3509 make_sized_popovers: impl FnOnce(
3510 Pixels,
3511 Pixels,
3512 bool,
3513 &mut Window,
3514 &mut App,
3515 ) -> Vec<(CursorPopoverType, AnyElement, Size<Pixels>)>,
3516 ) -> Option<(Vec<(CursorPopoverType, Bounds<Pixels>)>, bool)> {
3517 let text_style = TextStyleRefinement {
3518 line_height: Some(DefiniteLength::Fraction(
3519 BufferLineHeight::Comfortable.value(),
3520 )),
3521 ..Default::default()
3522 };
3523 window.with_text_style(Some(text_style), |window| {
3524 // If the max height won't fit below and there is more space above, put it above the line.
3525 let bottom_y_when_flipped = target_position.y - line_height;
3526 let available_above = bottom_y_when_flipped - text_hitbox.top();
3527 let available_below = text_hitbox.bottom() - target_position.y;
3528 let y_overflows_below = max_height > available_below;
3529 let mut y_flipped = y_overflows_below && available_above > available_below;
3530 let mut height = cmp::min(
3531 max_height,
3532 if y_flipped {
3533 available_above
3534 } else {
3535 available_below
3536 },
3537 );
3538
3539 // If the min height doesn't fit within text bounds, instead fit within the window.
3540 if height < min_height {
3541 let available_above = bottom_y_when_flipped;
3542 let available_below = viewport_bounds.bottom() - target_position.y;
3543 if available_below > min_height {
3544 y_flipped = false;
3545 height = min_height;
3546 } else if available_above > min_height {
3547 y_flipped = true;
3548 height = min_height;
3549 } else if available_above > available_below {
3550 y_flipped = true;
3551 height = available_above;
3552 } else {
3553 y_flipped = false;
3554 height = available_below;
3555 }
3556 }
3557
3558 let max_width_for_stable_x = viewport_bounds.right() - target_position.x;
3559
3560 // TODO: Use viewport_bounds.width as a max width so that it doesn't get clipped on the left
3561 // for very narrow windows.
3562 let popovers =
3563 make_sized_popovers(height, max_width_for_stable_x, y_flipped, window, cx);
3564 if popovers.is_empty() {
3565 return None;
3566 }
3567
3568 let max_width = popovers
3569 .iter()
3570 .map(|(_, _, size)| size.width)
3571 .max()
3572 .unwrap_or_default();
3573
3574 let mut current_position = gpui::Point {
3575 // Snap the right edge of the list to the right edge of the window if its horizontal bounds
3576 // overflow. Include space for the scrollbar.
3577 x: target_position
3578 .x
3579 .min((viewport_bounds.right() - max_width).max(Pixels::ZERO)),
3580 y: if y_flipped {
3581 bottom_y_when_flipped
3582 } else {
3583 target_position.y
3584 },
3585 };
3586
3587 let mut laid_out_popovers = popovers
3588 .into_iter()
3589 .map(|(popover_type, element, size)| {
3590 if y_flipped {
3591 current_position.y -= size.height;
3592 }
3593 let position = current_position;
3594 window.defer_draw(element, current_position, 1);
3595 if !y_flipped {
3596 current_position.y += size.height + MENU_GAP;
3597 } else {
3598 current_position.y -= MENU_GAP;
3599 }
3600 (popover_type, Bounds::new(position, size))
3601 })
3602 .collect::<Vec<_>>();
3603
3604 if y_flipped {
3605 laid_out_popovers.reverse();
3606 }
3607
3608 Some((laid_out_popovers, y_flipped))
3609 })
3610 }
3611
3612 #[allow(clippy::too_many_arguments)]
3613 fn layout_context_menu_aside(
3614 &self,
3615 y_flipped: bool,
3616 menu_bounds: Bounds<Pixels>,
3617 target_bounds: Bounds<Pixels>,
3618 max_target_bounds: Bounds<Pixels>,
3619 max_height: Pixels,
3620 must_place_above_or_below: bool,
3621 text_hitbox: &Hitbox,
3622 viewport_bounds: Bounds<Pixels>,
3623 window: &mut Window,
3624 cx: &mut App,
3625 ) {
3626 let available_within_viewport = target_bounds.space_within(&viewport_bounds);
3627 let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH
3628 && !must_place_above_or_below
3629 {
3630 let max_width = cmp::min(
3631 available_within_viewport.right - px(1.),
3632 MENU_ASIDE_MAX_WIDTH,
3633 );
3634 let Some(mut aside) = self.render_context_menu_aside(
3635 size(max_width, max_height - POPOVER_Y_PADDING),
3636 window,
3637 cx,
3638 ) else {
3639 return;
3640 };
3641 aside.layout_as_root(AvailableSpace::min_size(), window, cx);
3642 let right_position = point(target_bounds.right(), menu_bounds.origin.y);
3643 Some((aside, right_position))
3644 } else {
3645 let max_size = size(
3646 // TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
3647 // won't be needed here.
3648 cmp::min(
3649 cmp::max(menu_bounds.size.width - px(2.), MENU_ASIDE_MIN_WIDTH),
3650 viewport_bounds.right(),
3651 ),
3652 cmp::min(
3653 max_height,
3654 cmp::max(
3655 available_within_viewport.top,
3656 available_within_viewport.bottom,
3657 ),
3658 ) - POPOVER_Y_PADDING,
3659 );
3660 let Some(mut aside) = self.render_context_menu_aside(max_size, window, cx) else {
3661 return;
3662 };
3663 let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
3664
3665 let top_position = point(
3666 menu_bounds.origin.x,
3667 target_bounds.top() - actual_size.height,
3668 );
3669 let bottom_position = point(menu_bounds.origin.x, target_bounds.bottom());
3670
3671 let fit_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
3672 // Prefer to fit on the same side of the line as the menu, then on the other side of
3673 // the line.
3674 if !y_flipped && wanted.height < available.bottom {
3675 Some(bottom_position)
3676 } else if !y_flipped && wanted.height < available.top {
3677 Some(top_position)
3678 } else if y_flipped && wanted.height < available.top {
3679 Some(top_position)
3680 } else if y_flipped && wanted.height < available.bottom {
3681 Some(bottom_position)
3682 } else {
3683 None
3684 }
3685 };
3686
3687 // Prefer choosing a direction using max sizes rather than actual size for stability.
3688 let available_within_text = max_target_bounds.space_within(&text_hitbox.bounds);
3689 let wanted = size(MENU_ASIDE_MAX_WIDTH, max_height);
3690 let aside_position = fit_within(available_within_text, wanted)
3691 // Fallback: fit max size in window.
3692 .or_else(|| fit_within(max_target_bounds.space_within(&viewport_bounds), wanted))
3693 // Fallback: fit actual size in window.
3694 .or_else(|| fit_within(available_within_viewport, actual_size));
3695
3696 aside_position.map(|position| (aside, position))
3697 };
3698
3699 // Skip drawing if it doesn't fit anywhere.
3700 if let Some((aside, position)) = positioned_aside {
3701 window.defer_draw(aside, position, 2);
3702 }
3703 }
3704
3705 fn render_context_menu(
3706 &self,
3707 line_height: Pixels,
3708 height: Pixels,
3709 y_flipped: bool,
3710 window: &mut Window,
3711 cx: &mut App,
3712 ) -> Option<AnyElement> {
3713 let max_height_in_lines = ((height - POPOVER_Y_PADDING) / line_height).floor() as u32;
3714 self.editor.update(cx, |editor, cx| {
3715 editor.render_context_menu(&self.style, max_height_in_lines, y_flipped, window, cx)
3716 })
3717 }
3718
3719 fn render_context_menu_aside(
3720 &self,
3721 max_size: Size<Pixels>,
3722 window: &mut Window,
3723 cx: &mut App,
3724 ) -> Option<AnyElement> {
3725 if max_size.width < px(100.) || max_size.height < px(12.) {
3726 None
3727 } else {
3728 self.editor.update(cx, |editor, cx| {
3729 editor.render_context_menu_aside(max_size, window, cx)
3730 })
3731 }
3732 }
3733
3734 fn layout_mouse_context_menu(
3735 &self,
3736 editor_snapshot: &EditorSnapshot,
3737 visible_range: Range<DisplayRow>,
3738 content_origin: gpui::Point<Pixels>,
3739 window: &mut Window,
3740 cx: &mut App,
3741 ) -> Option<AnyElement> {
3742 let position = self.editor.update(cx, |editor, _cx| {
3743 let visible_start_point = editor.display_to_pixel_point(
3744 DisplayPoint::new(visible_range.start, 0),
3745 editor_snapshot,
3746 window,
3747 )?;
3748 let visible_end_point = editor.display_to_pixel_point(
3749 DisplayPoint::new(visible_range.end, 0),
3750 editor_snapshot,
3751 window,
3752 )?;
3753
3754 let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
3755 let (source_display_point, position) = match mouse_context_menu.position {
3756 MenuPosition::PinnedToScreen(point) => (None, point),
3757 MenuPosition::PinnedToEditor { source, offset } => {
3758 let source_display_point = source.to_display_point(editor_snapshot);
3759 let source_point = editor.to_pixel_point(source, editor_snapshot, window)?;
3760 let position = content_origin + source_point + offset;
3761 (Some(source_display_point), position)
3762 }
3763 };
3764
3765 let source_included = source_display_point.map_or(true, |source_display_point| {
3766 visible_range
3767 .to_inclusive()
3768 .contains(&source_display_point.row())
3769 });
3770 let position_included =
3771 visible_start_point.y <= position.y && position.y <= visible_end_point.y;
3772 if !source_included && !position_included {
3773 None
3774 } else {
3775 Some(position)
3776 }
3777 })?;
3778
3779 let text_style = TextStyleRefinement {
3780 line_height: Some(DefiniteLength::Fraction(
3781 BufferLineHeight::Comfortable.value(),
3782 )),
3783 ..Default::default()
3784 };
3785 window.with_text_style(Some(text_style), |window| {
3786 let mut element = self.editor.update(cx, |editor, _| {
3787 let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
3788 let context_menu = mouse_context_menu.context_menu.clone();
3789
3790 Some(
3791 deferred(
3792 anchored()
3793 .position(position)
3794 .child(context_menu)
3795 .anchor(Corner::TopLeft)
3796 .snap_to_window_with_margin(px(8.)),
3797 )
3798 .with_priority(1)
3799 .into_any(),
3800 )
3801 })?;
3802
3803 element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx);
3804 Some(element)
3805 })
3806 }
3807
3808 #[allow(clippy::too_many_arguments)]
3809 fn layout_hover_popovers(
3810 &self,
3811 snapshot: &EditorSnapshot,
3812 hitbox: &Hitbox,
3813 text_hitbox: &Hitbox,
3814 visible_display_row_range: Range<DisplayRow>,
3815 content_origin: gpui::Point<Pixels>,
3816 scroll_pixel_position: gpui::Point<Pixels>,
3817 line_layouts: &[LineWithInvisibles],
3818 line_height: Pixels,
3819 em_width: Pixels,
3820 window: &mut Window,
3821 cx: &mut App,
3822 ) {
3823 struct MeasuredHoverPopover {
3824 element: AnyElement,
3825 size: Size<Pixels>,
3826 horizontal_offset: Pixels,
3827 }
3828
3829 let max_size = size(
3830 (120. * em_width) // Default size
3831 .min(hitbox.size.width / 2.) // Shrink to half of the editor width
3832 .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
3833 (16. * line_height) // Default size
3834 .min(hitbox.size.height / 2.) // Shrink to half of the editor height
3835 .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
3836 );
3837
3838 let hover_popovers = self.editor.update(cx, |editor, cx| {
3839 editor
3840 .hover_state
3841 .render(snapshot, visible_display_row_range.clone(), max_size, cx)
3842 });
3843 let Some((position, hover_popovers)) = hover_popovers else {
3844 return;
3845 };
3846
3847 // This is safe because we check on layout whether the required row is available
3848 let hovered_row_layout =
3849 &line_layouts[position.row().minus(visible_display_row_range.start) as usize];
3850
3851 // Compute Hovered Point
3852 let x =
3853 hovered_row_layout.x_for_index(position.column() as usize) - scroll_pixel_position.x;
3854 let y = position.row().as_f32() * line_height - scroll_pixel_position.y;
3855 let hovered_point = content_origin + point(x, y);
3856
3857 let mut overall_height = Pixels::ZERO;
3858 let mut measured_hover_popovers = Vec::new();
3859 for mut hover_popover in hover_popovers {
3860 let size = hover_popover.layout_as_root(AvailableSpace::min_size(), window, cx);
3861 let horizontal_offset =
3862 (text_hitbox.top_right().x - (hovered_point.x + size.width)).min(Pixels::ZERO);
3863
3864 overall_height += HOVER_POPOVER_GAP + size.height;
3865
3866 measured_hover_popovers.push(MeasuredHoverPopover {
3867 element: hover_popover,
3868 size,
3869 horizontal_offset,
3870 });
3871 }
3872 overall_height += HOVER_POPOVER_GAP;
3873
3874 fn draw_occluder(
3875 width: Pixels,
3876 origin: gpui::Point<Pixels>,
3877 window: &mut Window,
3878 cx: &mut App,
3879 ) {
3880 let mut occlusion = div()
3881 .size_full()
3882 .occlude()
3883 .on_mouse_move(|_, _, cx| cx.stop_propagation())
3884 .into_any_element();
3885 occlusion.layout_as_root(size(width, HOVER_POPOVER_GAP).into(), window, cx);
3886 window.defer_draw(occlusion, origin, 2);
3887 }
3888
3889 if hovered_point.y > overall_height {
3890 // There is enough space above. Render popovers above the hovered point
3891 let mut current_y = hovered_point.y;
3892 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
3893 let size = popover.size;
3894 let popover_origin = point(
3895 hovered_point.x + popover.horizontal_offset,
3896 current_y - size.height,
3897 );
3898
3899 window.defer_draw(popover.element, popover_origin, 2);
3900 if position != itertools::Position::Last {
3901 let origin = point(popover_origin.x, popover_origin.y - HOVER_POPOVER_GAP);
3902 draw_occluder(size.width, origin, window, cx);
3903 }
3904
3905 current_y = popover_origin.y - HOVER_POPOVER_GAP;
3906 }
3907 } else {
3908 // There is not enough space above. Render popovers below the hovered point
3909 let mut current_y = hovered_point.y + line_height;
3910 for (position, popover) in measured_hover_popovers.into_iter().with_position() {
3911 let size = popover.size;
3912 let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
3913
3914 window.defer_draw(popover.element, popover_origin, 2);
3915 if position != itertools::Position::Last {
3916 let origin = point(popover_origin.x, popover_origin.y + size.height);
3917 draw_occluder(size.width, origin, window, cx);
3918 }
3919
3920 current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
3921 }
3922 }
3923 }
3924
3925 #[allow(clippy::too_many_arguments)]
3926 fn layout_diff_hunk_controls(
3927 &self,
3928 row_range: Range<DisplayRow>,
3929 row_infos: &[RowInfo],
3930 text_hitbox: &Hitbox,
3931 position_map: &PositionMap,
3932 newest_cursor_position: Option<DisplayPoint>,
3933 line_height: Pixels,
3934 scroll_pixel_position: gpui::Point<Pixels>,
3935 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
3936 editor: Entity<Editor>,
3937 window: &mut Window,
3938 cx: &mut App,
3939 ) -> Vec<AnyElement> {
3940 let point_for_position = position_map.point_for_position(window.mouse_position());
3941
3942 let mut controls = vec![];
3943
3944 let active_positions = [
3945 Some(point_for_position.previous_valid),
3946 newest_cursor_position,
3947 ];
3948
3949 for (hunk, _) in display_hunks {
3950 if let DisplayDiffHunk::Unfolded {
3951 display_row_range,
3952 multi_buffer_range,
3953 status,
3954 is_created_file,
3955 ..
3956 } = &hunk
3957 {
3958 if display_row_range.start < row_range.start
3959 || display_row_range.start >= row_range.end
3960 {
3961 continue;
3962 }
3963 let row_ix = (display_row_range.start - row_range.start).0 as usize;
3964 if row_infos[row_ix].diff_status.is_none() {
3965 continue;
3966 }
3967 if row_infos[row_ix]
3968 .diff_status
3969 .is_some_and(|status| status.is_added())
3970 && !status.is_added()
3971 {
3972 continue;
3973 }
3974 if active_positions
3975 .iter()
3976 .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row())))
3977 {
3978 let y = display_row_range.start.as_f32() * line_height
3979 + text_hitbox.bounds.top()
3980 - scroll_pixel_position.y;
3981
3982 let mut element = diff_hunk_controls(
3983 display_row_range.start.0,
3984 status,
3985 multi_buffer_range.clone(),
3986 *is_created_file,
3987 line_height,
3988 &editor,
3989 cx,
3990 );
3991 let size =
3992 element.layout_as_root(size(px(100.0), line_height).into(), window, cx);
3993
3994 let x = text_hitbox.bounds.right()
3995 - self.style.scrollbar_width
3996 - px(10.)
3997 - size.width;
3998
3999 window.with_absolute_element_offset(gpui::Point::new(x, y), |window| {
4000 element.prepaint(window, cx)
4001 });
4002 controls.push(element);
4003 }
4004 }
4005 }
4006
4007 controls
4008 }
4009
4010 #[allow(clippy::too_many_arguments)]
4011 fn layout_signature_help(
4012 &self,
4013 hitbox: &Hitbox,
4014 content_origin: gpui::Point<Pixels>,
4015 scroll_pixel_position: gpui::Point<Pixels>,
4016 newest_selection_head: Option<DisplayPoint>,
4017 start_row: DisplayRow,
4018 line_layouts: &[LineWithInvisibles],
4019 line_height: Pixels,
4020 em_width: Pixels,
4021 window: &mut Window,
4022 cx: &mut App,
4023 ) {
4024 if !self.editor.focus_handle(cx).is_focused(window) {
4025 return;
4026 }
4027 let Some(newest_selection_head) = newest_selection_head else {
4028 return;
4029 };
4030 let selection_row = newest_selection_head.row();
4031 if selection_row < start_row {
4032 return;
4033 }
4034 let Some(cursor_row_layout) = line_layouts.get(selection_row.minus(start_row) as usize)
4035 else {
4036 return;
4037 };
4038
4039 let start_x = cursor_row_layout.x_for_index(newest_selection_head.column() as usize)
4040 - scroll_pixel_position.x
4041 + content_origin.x;
4042 let start_y =
4043 selection_row.as_f32() * line_height + content_origin.y - scroll_pixel_position.y;
4044
4045 let max_size = size(
4046 (120. * em_width) // Default size
4047 .min(hitbox.size.width / 2.) // Shrink to half of the editor width
4048 .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
4049 (16. * line_height) // Default size
4050 .min(hitbox.size.height / 2.) // Shrink to half of the editor height
4051 .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
4052 );
4053
4054 let maybe_element = self.editor.update(cx, |editor, cx| {
4055 if let Some(popover) = editor.signature_help_state.popover_mut() {
4056 let element = popover.render(max_size, cx);
4057 Some(element)
4058 } else {
4059 None
4060 }
4061 });
4062 if let Some(mut element) = maybe_element {
4063 let window_size = window.viewport_size();
4064 let size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
4065 let mut point = point(start_x, start_y - size.height);
4066
4067 // Adjusting to ensure the popover does not overflow in the X-axis direction.
4068 if point.x + size.width >= window_size.width {
4069 point.x = window_size.width - size.width;
4070 }
4071
4072 window.defer_draw(element, point, 1)
4073 }
4074 }
4075
4076 fn paint_background(&self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
4077 window.paint_layer(layout.hitbox.bounds, |window| {
4078 let scroll_top = layout.position_map.snapshot.scroll_position().y;
4079 let gutter_bg = cx.theme().colors().editor_gutter_background;
4080 window.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg));
4081 window.paint_quad(fill(
4082 layout.position_map.text_hitbox.bounds,
4083 self.style.background,
4084 ));
4085
4086 if let EditorMode::Full = layout.mode {
4087 let mut active_rows = layout.active_rows.iter().peekable();
4088 while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
4089 let mut end_row = start_row.0;
4090 while active_rows
4091 .peek()
4092 .map_or(false, |(active_row, has_selection)| {
4093 active_row.0 == end_row + 1
4094 && *has_selection == contains_non_empty_selection
4095 })
4096 {
4097 active_rows.next().unwrap();
4098 end_row += 1;
4099 }
4100
4101 if !contains_non_empty_selection {
4102 let highlight_h_range =
4103 match layout.position_map.snapshot.current_line_highlight {
4104 CurrentLineHighlight::Gutter => Some(Range {
4105 start: layout.hitbox.left(),
4106 end: layout.gutter_hitbox.right(),
4107 }),
4108 CurrentLineHighlight::Line => Some(Range {
4109 start: layout.position_map.text_hitbox.bounds.left(),
4110 end: layout.position_map.text_hitbox.bounds.right(),
4111 }),
4112 CurrentLineHighlight::All => Some(Range {
4113 start: layout.hitbox.left(),
4114 end: layout.hitbox.right(),
4115 }),
4116 CurrentLineHighlight::None => None,
4117 };
4118 if let Some(range) = highlight_h_range {
4119 let active_line_bg = cx.theme().colors().editor_active_line_background;
4120 let bounds = Bounds {
4121 origin: point(
4122 range.start,
4123 layout.hitbox.origin.y
4124 + (start_row.as_f32() - scroll_top)
4125 * layout.position_map.line_height,
4126 ),
4127 size: size(
4128 range.end - range.start,
4129 layout.position_map.line_height
4130 * (end_row - start_row.0 + 1) as f32,
4131 ),
4132 };
4133 window.paint_quad(fill(bounds, active_line_bg));
4134 }
4135 }
4136 }
4137
4138 let mut paint_highlight = |highlight_row_start: DisplayRow,
4139 highlight_row_end: DisplayRow,
4140 highlight: crate::LineHighlight,
4141 edges| {
4142 let origin = point(
4143 layout.hitbox.origin.x,
4144 layout.hitbox.origin.y
4145 + (highlight_row_start.as_f32() - scroll_top)
4146 * layout.position_map.line_height,
4147 );
4148 let size = size(
4149 layout.hitbox.size.width,
4150 layout.position_map.line_height
4151 * highlight_row_end.next_row().minus(highlight_row_start) as f32,
4152 );
4153 let mut quad = fill(Bounds { origin, size }, highlight.background);
4154 if let Some(border_color) = highlight.border {
4155 quad.border_color = border_color;
4156 quad.border_widths = edges
4157 }
4158 window.paint_quad(quad);
4159 };
4160
4161 let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
4162 None;
4163 for (&new_row, &new_background) in &layout.highlighted_rows {
4164 match &mut current_paint {
4165 Some((current_background, current_range, mut edges)) => {
4166 let current_background = *current_background;
4167 let new_range_started = current_background != new_background
4168 || current_range.end.next_row() != new_row;
4169 if new_range_started {
4170 if current_range.end.next_row() == new_row {
4171 edges.bottom = px(0.);
4172 };
4173 paint_highlight(
4174 current_range.start,
4175 current_range.end,
4176 current_background,
4177 edges,
4178 );
4179 let edges = Edges {
4180 top: if current_range.end.next_row() != new_row {
4181 px(1.)
4182 } else {
4183 px(0.)
4184 },
4185 bottom: px(1.),
4186 ..Default::default()
4187 };
4188 current_paint = Some((new_background, new_row..new_row, edges));
4189 continue;
4190 } else {
4191 current_range.end = current_range.end.next_row();
4192 }
4193 }
4194 None => {
4195 let edges = Edges {
4196 top: px(1.),
4197 bottom: px(1.),
4198 ..Default::default()
4199 };
4200 current_paint = Some((new_background, new_row..new_row, edges))
4201 }
4202 };
4203 }
4204 if let Some((color, range, edges)) = current_paint {
4205 paint_highlight(range.start, range.end, color, edges);
4206 }
4207
4208 let scroll_left =
4209 layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width;
4210
4211 for (wrap_position, active) in layout.wrap_guides.iter() {
4212 let x = (layout.position_map.text_hitbox.origin.x
4213 + *wrap_position
4214 + layout.position_map.em_width / 2.)
4215 - scroll_left;
4216
4217 let show_scrollbars = {
4218 let (scrollbar_x, scrollbar_y) = &layout.scrollbars_layout.as_xy();
4219
4220 scrollbar_x.as_ref().map_or(false, |sx| sx.visible)
4221 || scrollbar_y.as_ref().map_or(false, |sy| sy.visible)
4222 };
4223
4224 if x < layout.position_map.text_hitbox.origin.x
4225 || (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds))
4226 {
4227 continue;
4228 }
4229
4230 let color = if *active {
4231 cx.theme().colors().editor_active_wrap_guide
4232 } else {
4233 cx.theme().colors().editor_wrap_guide
4234 };
4235 window.paint_quad(fill(
4236 Bounds {
4237 origin: point(x, layout.position_map.text_hitbox.origin.y),
4238 size: size(px(1.), layout.position_map.text_hitbox.size.height),
4239 },
4240 color,
4241 ));
4242 }
4243 }
4244 })
4245 }
4246
4247 fn paint_indent_guides(
4248 &mut self,
4249 layout: &mut EditorLayout,
4250 window: &mut Window,
4251 cx: &mut App,
4252 ) {
4253 let Some(indent_guides) = &layout.indent_guides else {
4254 return;
4255 };
4256
4257 let faded_color = |color: Hsla, alpha: f32| {
4258 let mut faded = color;
4259 faded.a = alpha;
4260 faded
4261 };
4262
4263 for indent_guide in indent_guides {
4264 let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
4265 let settings = indent_guide.settings;
4266
4267 // TODO fixed for now, expose them through themes later
4268 const INDENT_AWARE_ALPHA: f32 = 0.2;
4269 const INDENT_AWARE_ACTIVE_ALPHA: f32 = 0.4;
4270 const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
4271 const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
4272
4273 let line_color = match (settings.coloring, indent_guide.active) {
4274 (IndentGuideColoring::Disabled, _) => None,
4275 (IndentGuideColoring::Fixed, false) => {
4276 Some(cx.theme().colors().editor_indent_guide)
4277 }
4278 (IndentGuideColoring::Fixed, true) => {
4279 Some(cx.theme().colors().editor_indent_guide_active)
4280 }
4281 (IndentGuideColoring::IndentAware, false) => {
4282 Some(faded_color(indent_accent_colors, INDENT_AWARE_ALPHA))
4283 }
4284 (IndentGuideColoring::IndentAware, true) => {
4285 Some(faded_color(indent_accent_colors, INDENT_AWARE_ACTIVE_ALPHA))
4286 }
4287 };
4288
4289 let background_color = match (settings.background_coloring, indent_guide.active) {
4290 (IndentGuideBackgroundColoring::Disabled, _) => None,
4291 (IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
4292 indent_accent_colors,
4293 INDENT_AWARE_BACKGROUND_ALPHA,
4294 )),
4295 (IndentGuideBackgroundColoring::IndentAware, true) => Some(faded_color(
4296 indent_accent_colors,
4297 INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA,
4298 )),
4299 };
4300
4301 let requested_line_width = if indent_guide.active {
4302 settings.active_line_width
4303 } else {
4304 settings.line_width
4305 }
4306 .clamp(1, 10);
4307 let mut line_indicator_width = 0.;
4308 if let Some(color) = line_color {
4309 window.paint_quad(fill(
4310 Bounds {
4311 origin: indent_guide.origin,
4312 size: size(px(requested_line_width as f32), indent_guide.length),
4313 },
4314 color,
4315 ));
4316 line_indicator_width = requested_line_width as f32;
4317 }
4318
4319 if let Some(color) = background_color {
4320 let width = indent_guide.single_indent_width - px(line_indicator_width);
4321 window.paint_quad(fill(
4322 Bounds {
4323 origin: point(
4324 indent_guide.origin.x + px(line_indicator_width),
4325 indent_guide.origin.y,
4326 ),
4327 size: size(width, indent_guide.length),
4328 },
4329 color,
4330 ));
4331 }
4332 }
4333 }
4334
4335 fn paint_line_numbers(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4336 let is_singleton = self.editor.read(cx).is_singleton(cx);
4337
4338 let line_height = layout.position_map.line_height;
4339 window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
4340
4341 for LineNumberLayout {
4342 shaped_line,
4343 hitbox,
4344 display_row,
4345 } in layout.line_numbers.values()
4346 {
4347 let Some(hitbox) = hitbox else {
4348 continue;
4349 };
4350
4351 let is_active = layout.active_rows.contains_key(&display_row);
4352
4353 let color = if is_active {
4354 cx.theme().colors().editor_active_line_number
4355 } else if !is_singleton && hitbox.is_hovered(window) {
4356 cx.theme().colors().editor_hover_line_number
4357 } else {
4358 cx.theme().colors().editor_line_number
4359 };
4360
4361 let Some(line) = self
4362 .shape_line_number(shaped_line.text.clone(), color, window)
4363 .log_err()
4364 else {
4365 continue;
4366 };
4367 let Some(()) = line.paint(hitbox.origin, line_height, window, cx).log_err() else {
4368 continue;
4369 };
4370 // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
4371 // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
4372 if is_singleton {
4373 window.set_cursor_style(CursorStyle::IBeam, &hitbox);
4374 } else {
4375 window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
4376 }
4377 }
4378 }
4379
4380 fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4381 if layout.display_hunks.is_empty() {
4382 return;
4383 }
4384
4385 let line_height = layout.position_map.line_height;
4386 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
4387 for (hunk, hitbox) in &layout.display_hunks {
4388 let hunk_to_paint = match hunk {
4389 DisplayDiffHunk::Folded { .. } => {
4390 let hunk_bounds = Self::diff_hunk_bounds(
4391 &layout.position_map.snapshot,
4392 line_height,
4393 layout.gutter_hitbox.bounds,
4394 hunk,
4395 );
4396 Some((
4397 hunk_bounds,
4398 cx.theme().colors().version_control_modified,
4399 Corners::all(px(0.)),
4400 DiffHunkStatus::modified_none(),
4401 ))
4402 }
4403 DisplayDiffHunk::Unfolded {
4404 status,
4405 display_row_range,
4406 ..
4407 } => hitbox.as_ref().map(|hunk_hitbox| match status.kind {
4408 DiffHunkStatusKind::Added => (
4409 hunk_hitbox.bounds,
4410 cx.theme().colors().version_control_added,
4411 Corners::all(px(0.)),
4412 *status,
4413 ),
4414 DiffHunkStatusKind::Modified => (
4415 hunk_hitbox.bounds,
4416 cx.theme().colors().version_control_modified,
4417 Corners::all(px(0.)),
4418 *status,
4419 ),
4420 DiffHunkStatusKind::Deleted if !display_row_range.is_empty() => (
4421 hunk_hitbox.bounds,
4422 cx.theme().colors().version_control_deleted,
4423 Corners::all(px(0.)),
4424 *status,
4425 ),
4426 DiffHunkStatusKind::Deleted => (
4427 Bounds::new(
4428 point(
4429 hunk_hitbox.origin.x - hunk_hitbox.size.width,
4430 hunk_hitbox.origin.y,
4431 ),
4432 size(hunk_hitbox.size.width * px(2.), hunk_hitbox.size.height),
4433 ),
4434 cx.theme().colors().version_control_deleted,
4435 Corners::all(1. * line_height),
4436 *status,
4437 ),
4438 }),
4439 };
4440
4441 if let Some((hunk_bounds, background_color, corner_radii, _)) = hunk_to_paint {
4442 // Flatten the background color with the editor color to prevent
4443 // elements below transparent hunks from showing through
4444 let flattened_background_color = cx
4445 .theme()
4446 .colors()
4447 .editor_background
4448 .blend(background_color);
4449
4450 window.paint_quad(quad(
4451 hunk_bounds,
4452 corner_radii,
4453 flattened_background_color,
4454 Edges::default(),
4455 transparent_black(),
4456 ));
4457 }
4458 }
4459 });
4460 }
4461
4462 fn diff_hunk_bounds(
4463 snapshot: &EditorSnapshot,
4464 line_height: Pixels,
4465 gutter_bounds: Bounds<Pixels>,
4466 hunk: &DisplayDiffHunk,
4467 ) -> Bounds<Pixels> {
4468 let scroll_position = snapshot.scroll_position();
4469 let scroll_top = scroll_position.y * line_height;
4470 let gutter_strip_width = (0.275 * line_height).floor();
4471
4472 match hunk {
4473 DisplayDiffHunk::Folded { display_row, .. } => {
4474 let start_y = display_row.as_f32() * line_height - scroll_top;
4475 let end_y = start_y + line_height;
4476 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
4477 let highlight_size = size(gutter_strip_width, end_y - start_y);
4478 Bounds::new(highlight_origin, highlight_size)
4479 }
4480 DisplayDiffHunk::Unfolded {
4481 display_row_range,
4482 status,
4483 ..
4484 } => {
4485 if status.is_deleted() && display_row_range.is_empty() {
4486 let row = display_row_range.start;
4487
4488 let offset = line_height / 2.;
4489 let start_y = row.as_f32() * line_height - offset - scroll_top;
4490 let end_y = start_y + line_height;
4491
4492 let width = (0.35 * line_height).floor();
4493 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
4494 let highlight_size = size(width, end_y - start_y);
4495 Bounds::new(highlight_origin, highlight_size)
4496 } else {
4497 let start_row = display_row_range.start;
4498 let end_row = display_row_range.end;
4499 // If we're in a multibuffer, row range span might include an
4500 // excerpt header, so if we were to draw the marker straight away,
4501 // the hunk might include the rows of that header.
4502 // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
4503 // Instead, we simply check whether the range we're dealing with includes
4504 // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header.
4505 let end_row_in_current_excerpt = snapshot
4506 .blocks_in_range(start_row..end_row)
4507 .find_map(|(start_row, block)| {
4508 if matches!(block, Block::ExcerptBoundary { .. }) {
4509 Some(start_row)
4510 } else {
4511 None
4512 }
4513 })
4514 .unwrap_or(end_row);
4515
4516 let start_y = start_row.as_f32() * line_height - scroll_top;
4517 let end_y = end_row_in_current_excerpt.as_f32() * line_height - scroll_top;
4518
4519 let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
4520 let highlight_size = size(gutter_strip_width, end_y - start_y);
4521 Bounds::new(highlight_origin, highlight_size)
4522 }
4523 }
4524 }
4525 }
4526
4527 fn paint_gutter_indicators(
4528 &self,
4529 layout: &mut EditorLayout,
4530 window: &mut Window,
4531 cx: &mut App,
4532 ) {
4533 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
4534 window.with_element_namespace("crease_toggles", |window| {
4535 for crease_toggle in layout.crease_toggles.iter_mut().flatten() {
4536 crease_toggle.paint(window, cx);
4537 }
4538 });
4539
4540 for test_indicator in layout.test_indicators.iter_mut() {
4541 test_indicator.paint(window, cx);
4542 }
4543
4544 if let Some(indicator) = layout.code_actions_indicator.as_mut() {
4545 indicator.paint(window, cx);
4546 }
4547 });
4548 }
4549
4550 fn paint_gutter_highlights(
4551 &self,
4552 layout: &mut EditorLayout,
4553 window: &mut Window,
4554 cx: &mut App,
4555 ) {
4556 for (_, hunk_hitbox) in &layout.display_hunks {
4557 if let Some(hunk_hitbox) = hunk_hitbox {
4558 if !self
4559 .editor
4560 .read(cx)
4561 .buffer()
4562 .read(cx)
4563 .all_diff_hunks_expanded()
4564 {
4565 window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
4566 }
4567 }
4568 }
4569
4570 let show_git_gutter = layout
4571 .position_map
4572 .snapshot
4573 .show_git_diff_gutter
4574 .unwrap_or_else(|| {
4575 matches!(
4576 ProjectSettings::get_global(cx).git.git_gutter,
4577 Some(GitGutterSetting::TrackedFiles)
4578 )
4579 });
4580 if show_git_gutter {
4581 Self::paint_gutter_diff_hunks(layout, window, cx)
4582 }
4583
4584 let highlight_width = 0.275 * layout.position_map.line_height;
4585 let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
4586 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
4587 for (range, color) in &layout.highlighted_gutter_ranges {
4588 let start_row = if range.start.row() < layout.visible_display_row_range.start {
4589 layout.visible_display_row_range.start - DisplayRow(1)
4590 } else {
4591 range.start.row()
4592 };
4593 let end_row = if range.end.row() > layout.visible_display_row_range.end {
4594 layout.visible_display_row_range.end + DisplayRow(1)
4595 } else {
4596 range.end.row()
4597 };
4598
4599 let start_y = layout.gutter_hitbox.top()
4600 + start_row.0 as f32 * layout.position_map.line_height
4601 - layout.position_map.scroll_pixel_position.y;
4602 let end_y = layout.gutter_hitbox.top()
4603 + (end_row.0 + 1) as f32 * layout.position_map.line_height
4604 - layout.position_map.scroll_pixel_position.y;
4605 let bounds = Bounds::from_corners(
4606 point(layout.gutter_hitbox.left(), start_y),
4607 point(layout.gutter_hitbox.left() + highlight_width, end_y),
4608 );
4609 window.paint_quad(fill(bounds, *color).corner_radii(highlight_corner_radii));
4610 }
4611 });
4612 }
4613
4614 fn paint_blamed_display_rows(
4615 &self,
4616 layout: &mut EditorLayout,
4617 window: &mut Window,
4618 cx: &mut App,
4619 ) {
4620 let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
4621 return;
4622 };
4623
4624 window.paint_layer(layout.gutter_hitbox.bounds, |window| {
4625 for mut blame_element in blamed_display_rows.into_iter() {
4626 blame_element.paint(window, cx);
4627 }
4628 })
4629 }
4630
4631 fn paint_text(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4632 window.with_content_mask(
4633 Some(ContentMask {
4634 bounds: layout.position_map.text_hitbox.bounds,
4635 }),
4636 |window| {
4637 let cursor_style = if self
4638 .editor
4639 .read(cx)
4640 .hovered_link_state
4641 .as_ref()
4642 .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
4643 {
4644 CursorStyle::PointingHand
4645 } else {
4646 CursorStyle::IBeam
4647 };
4648 window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
4649
4650 let invisible_display_ranges = self.paint_highlights(layout, window);
4651 self.paint_lines(&invisible_display_ranges, layout, window, cx);
4652 self.paint_redactions(layout, window);
4653 self.paint_cursors(layout, window, cx);
4654 self.paint_inline_diagnostics(layout, window, cx);
4655 self.paint_inline_blame(layout, window, cx);
4656 self.paint_diff_hunk_controls(layout, window, cx);
4657 window.with_element_namespace("crease_trailers", |window| {
4658 for trailer in layout.crease_trailers.iter_mut().flatten() {
4659 trailer.element.paint(window, cx);
4660 }
4661 });
4662 },
4663 )
4664 }
4665
4666 fn paint_highlights(
4667 &mut self,
4668 layout: &mut EditorLayout,
4669 window: &mut Window,
4670 ) -> SmallVec<[Range<DisplayPoint>; 32]> {
4671 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
4672 let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
4673 let line_end_overshoot = 0.15 * layout.position_map.line_height;
4674 for (range, color) in &layout.highlighted_ranges {
4675 self.paint_highlighted_range(
4676 range.clone(),
4677 *color,
4678 Pixels::ZERO,
4679 line_end_overshoot,
4680 layout,
4681 window,
4682 );
4683 }
4684
4685 let corner_radius = 0.15 * layout.position_map.line_height;
4686
4687 for (player_color, selections) in &layout.selections {
4688 for selection in selections.iter() {
4689 self.paint_highlighted_range(
4690 selection.range.clone(),
4691 player_color.selection,
4692 corner_radius,
4693 corner_radius * 2.,
4694 layout,
4695 window,
4696 );
4697
4698 if selection.is_local && !selection.range.is_empty() {
4699 invisible_display_ranges.push(selection.range.clone());
4700 }
4701 }
4702 }
4703 invisible_display_ranges
4704 })
4705 }
4706
4707 fn paint_lines(
4708 &mut self,
4709 invisible_display_ranges: &[Range<DisplayPoint>],
4710 layout: &mut EditorLayout,
4711 window: &mut Window,
4712 cx: &mut App,
4713 ) {
4714 let whitespace_setting = self
4715 .editor
4716 .read(cx)
4717 .buffer
4718 .read(cx)
4719 .language_settings(cx)
4720 .show_whitespaces;
4721
4722 for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
4723 let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
4724 line_with_invisibles.draw(
4725 layout,
4726 row,
4727 layout.content_origin,
4728 whitespace_setting,
4729 invisible_display_ranges,
4730 window,
4731 cx,
4732 )
4733 }
4734
4735 for line_element in &mut layout.line_elements {
4736 line_element.paint(window, cx);
4737 }
4738 }
4739
4740 fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
4741 if layout.redacted_ranges.is_empty() {
4742 return;
4743 }
4744
4745 let line_end_overshoot = layout.line_end_overshoot();
4746
4747 // A softer than perfect black
4748 let redaction_color = gpui::rgb(0x0e1111);
4749
4750 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
4751 for range in layout.redacted_ranges.iter() {
4752 self.paint_highlighted_range(
4753 range.clone(),
4754 redaction_color.into(),
4755 Pixels::ZERO,
4756 line_end_overshoot,
4757 layout,
4758 window,
4759 );
4760 }
4761 });
4762 }
4763
4764 fn paint_cursors(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4765 for cursor in &mut layout.visible_cursors {
4766 cursor.paint(layout.content_origin, window, cx);
4767 }
4768 }
4769
4770 fn paint_scrollbars(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
4771 let (scrollbar_x, scrollbar_y) = layout.scrollbars_layout.as_xy();
4772
4773 if let Some(scrollbar_layout) = scrollbar_x {
4774 let hitbox = scrollbar_layout.hitbox.clone();
4775 let text_unit_size = scrollbar_layout.text_unit_size;
4776 let visible_range = scrollbar_layout.visible_range.clone();
4777 let thumb_bounds = scrollbar_layout.thumb_bounds();
4778
4779 if scrollbar_layout.visible {
4780 window.paint_layer(hitbox.bounds, |window| {
4781 window.paint_quad(quad(
4782 hitbox.bounds,
4783 Corners::default(),
4784 cx.theme().colors().scrollbar_track_background,
4785 Edges {
4786 top: Pixels::ZERO,
4787 right: Pixels::ZERO,
4788 bottom: Pixels::ZERO,
4789 left: Pixels::ZERO,
4790 },
4791 cx.theme().colors().scrollbar_track_border,
4792 ));
4793
4794 window.paint_quad(quad(
4795 thumb_bounds,
4796 Corners::default(),
4797 cx.theme().colors().scrollbar_thumb_background,
4798 Edges {
4799 top: Pixels::ZERO,
4800 right: Pixels::ZERO,
4801 bottom: Pixels::ZERO,
4802 left: ScrollbarLayout::BORDER_WIDTH,
4803 },
4804 cx.theme().colors().scrollbar_thumb_border,
4805 ));
4806 })
4807 }
4808
4809 window.set_cursor_style(CursorStyle::Arrow, &hitbox);
4810
4811 window.on_mouse_event({
4812 let editor = self.editor.clone();
4813
4814 // there may be a way to avoid this clone
4815 let hitbox = hitbox.clone();
4816
4817 let mut mouse_position = window.mouse_position();
4818 move |event: &MouseMoveEvent, phase, window, cx| {
4819 if phase == DispatchPhase::Capture {
4820 return;
4821 }
4822
4823 editor.update(cx, |editor, cx| {
4824 if event.pressed_button == Some(MouseButton::Left)
4825 && editor
4826 .scroll_manager
4827 .is_dragging_scrollbar(Axis::Horizontal)
4828 {
4829 let x = mouse_position.x;
4830 let new_x = event.position.x;
4831 if (hitbox.left()..hitbox.right()).contains(&x) {
4832 let mut position = editor.scroll_position(cx);
4833
4834 position.x += (new_x - x) / text_unit_size;
4835 if position.x < 0.0 {
4836 position.x = 0.0;
4837 }
4838 editor.set_scroll_position(position, window, cx);
4839 }
4840
4841 cx.stop_propagation();
4842 } else {
4843 editor.scroll_manager.set_is_dragging_scrollbar(
4844 Axis::Horizontal,
4845 false,
4846 cx,
4847 );
4848
4849 if hitbox.is_hovered(window) {
4850 editor.scroll_manager.show_scrollbar(window, cx);
4851 }
4852 }
4853 mouse_position = event.position;
4854 })
4855 }
4856 });
4857
4858 if self
4859 .editor
4860 .read(cx)
4861 .scroll_manager
4862 .is_dragging_scrollbar(Axis::Horizontal)
4863 {
4864 window.on_mouse_event({
4865 let editor = self.editor.clone();
4866 move |_: &MouseUpEvent, phase, _, cx| {
4867 if phase == DispatchPhase::Capture {
4868 return;
4869 }
4870
4871 editor.update(cx, |editor, cx| {
4872 editor.scroll_manager.set_is_dragging_scrollbar(
4873 Axis::Horizontal,
4874 false,
4875 cx,
4876 );
4877 cx.stop_propagation();
4878 });
4879 }
4880 });
4881 } else {
4882 window.on_mouse_event({
4883 let editor = self.editor.clone();
4884
4885 move |event: &MouseDownEvent, phase, window, cx| {
4886 if phase == DispatchPhase::Capture || !hitbox.is_hovered(window) {
4887 return;
4888 }
4889
4890 editor.update(cx, |editor, cx| {
4891 editor.scroll_manager.set_is_dragging_scrollbar(
4892 Axis::Horizontal,
4893 true,
4894 cx,
4895 );
4896
4897 let x = event.position.x;
4898
4899 if x < thumb_bounds.left() || thumb_bounds.right() < x {
4900 let center_row =
4901 ((x - hitbox.left()) / text_unit_size).round() as u32;
4902 let top_row = center_row.saturating_sub(
4903 (visible_range.end - visible_range.start) as u32 / 2,
4904 );
4905
4906 let mut position = editor.scroll_position(cx);
4907 position.x = top_row as f32;
4908
4909 editor.set_scroll_position(position, window, cx);
4910 } else {
4911 editor.scroll_manager.show_scrollbar(window, cx);
4912 }
4913
4914 cx.stop_propagation();
4915 });
4916 }
4917 });
4918 }
4919 }
4920
4921 if let Some(scrollbar_layout) = scrollbar_y {
4922 let hitbox = scrollbar_layout.hitbox.clone();
4923 let text_unit_size = scrollbar_layout.text_unit_size;
4924 let visible_range = scrollbar_layout.visible_range.clone();
4925 let thumb_bounds = scrollbar_layout.thumb_bounds();
4926
4927 if scrollbar_layout.visible {
4928 window.paint_layer(hitbox.bounds, |window| {
4929 window.paint_quad(quad(
4930 hitbox.bounds,
4931 Corners::default(),
4932 cx.theme().colors().scrollbar_track_background,
4933 Edges {
4934 top: Pixels::ZERO,
4935 right: Pixels::ZERO,
4936 bottom: Pixels::ZERO,
4937 left: ScrollbarLayout::BORDER_WIDTH,
4938 },
4939 cx.theme().colors().scrollbar_track_border,
4940 ));
4941
4942 let fast_markers =
4943 self.collect_fast_scrollbar_markers(layout, &scrollbar_layout, cx);
4944 // Refresh slow scrollbar markers in the background. Below, we paint whatever markers have already been computed.
4945 self.refresh_slow_scrollbar_markers(layout, &scrollbar_layout, window, cx);
4946
4947 let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone();
4948 for marker in markers.iter().chain(&fast_markers) {
4949 let mut marker = marker.clone();
4950 marker.bounds.origin += hitbox.origin;
4951 window.paint_quad(marker);
4952 }
4953
4954 window.paint_quad(quad(
4955 thumb_bounds,
4956 Corners::default(),
4957 cx.theme().colors().scrollbar_thumb_background,
4958 Edges {
4959 top: Pixels::ZERO,
4960 right: Pixels::ZERO,
4961 bottom: Pixels::ZERO,
4962 left: ScrollbarLayout::BORDER_WIDTH,
4963 },
4964 cx.theme().colors().scrollbar_thumb_border,
4965 ));
4966 });
4967 }
4968
4969 window.set_cursor_style(CursorStyle::Arrow, &hitbox);
4970
4971 window.on_mouse_event({
4972 let editor = self.editor.clone();
4973
4974 let hitbox = hitbox.clone();
4975
4976 let mut mouse_position = window.mouse_position();
4977 move |event: &MouseMoveEvent, phase, window, cx| {
4978 if phase == DispatchPhase::Capture {
4979 return;
4980 }
4981
4982 editor.update(cx, |editor, cx| {
4983 if event.pressed_button == Some(MouseButton::Left)
4984 && editor.scroll_manager.is_dragging_scrollbar(Axis::Vertical)
4985 {
4986 let y = mouse_position.y;
4987 let new_y = event.position.y;
4988 if (hitbox.top()..hitbox.bottom()).contains(&y) {
4989 let mut position = editor.scroll_position(cx);
4990 position.y += (new_y - y) / text_unit_size;
4991 if position.y < 0.0 {
4992 position.y = 0.0;
4993 }
4994 editor.set_scroll_position(position, window, cx);
4995 }
4996 } else {
4997 editor.scroll_manager.set_is_dragging_scrollbar(
4998 Axis::Vertical,
4999 false,
5000 cx,
5001 );
5002
5003 if hitbox.is_hovered(window) {
5004 editor.scroll_manager.show_scrollbar(window, cx);
5005 }
5006 }
5007 mouse_position = event.position;
5008 })
5009 }
5010 });
5011
5012 if self
5013 .editor
5014 .read(cx)
5015 .scroll_manager
5016 .is_dragging_scrollbar(Axis::Vertical)
5017 {
5018 window.on_mouse_event({
5019 let editor = self.editor.clone();
5020 move |_: &MouseUpEvent, phase, _, cx| {
5021 if phase == DispatchPhase::Capture {
5022 return;
5023 }
5024
5025 editor.update(cx, |editor, cx| {
5026 editor.scroll_manager.set_is_dragging_scrollbar(
5027 Axis::Vertical,
5028 false,
5029 cx,
5030 );
5031 cx.stop_propagation();
5032 });
5033 }
5034 });
5035 } else {
5036 window.on_mouse_event({
5037 let editor = self.editor.clone();
5038
5039 move |event: &MouseDownEvent, phase, window, cx| {
5040 if phase == DispatchPhase::Capture || !hitbox.is_hovered(window) {
5041 return;
5042 }
5043
5044 editor.update(cx, |editor, cx| {
5045 editor.scroll_manager.set_is_dragging_scrollbar(
5046 Axis::Vertical,
5047 true,
5048 cx,
5049 );
5050
5051 let y = event.position.y;
5052 if y < thumb_bounds.top() || thumb_bounds.bottom() < y {
5053 let center_row =
5054 ((y - hitbox.top()) / text_unit_size).round() as u32;
5055 let top_row = center_row.saturating_sub(
5056 (visible_range.end - visible_range.start) as u32 / 2,
5057 );
5058 let mut position = editor.scroll_position(cx);
5059 position.y = top_row as f32;
5060 editor.set_scroll_position(position, window, cx);
5061 } else {
5062 editor.scroll_manager.show_scrollbar(window, cx);
5063 }
5064
5065 cx.stop_propagation();
5066 });
5067 }
5068 });
5069 }
5070 }
5071 }
5072
5073 fn collect_fast_scrollbar_markers(
5074 &self,
5075 layout: &EditorLayout,
5076 scrollbar_layout: &ScrollbarLayout,
5077 cx: &mut App,
5078 ) -> Vec<PaintQuad> {
5079 const LIMIT: usize = 100;
5080 if !EditorSettings::get_global(cx).scrollbar.cursors || layout.cursors.len() > LIMIT {
5081 return vec![];
5082 }
5083 let cursor_ranges = layout
5084 .cursors
5085 .iter()
5086 .map(|(point, color)| ColoredRange {
5087 start: point.row(),
5088 end: point.row(),
5089 color: *color,
5090 })
5091 .collect_vec();
5092 scrollbar_layout.marker_quads_for_ranges(cursor_ranges, None)
5093 }
5094
5095 fn refresh_slow_scrollbar_markers(
5096 &self,
5097 layout: &EditorLayout,
5098 scrollbar_layout: &ScrollbarLayout,
5099 window: &mut Window,
5100 cx: &mut App,
5101 ) {
5102 self.editor.update(cx, |editor, cx| {
5103 if !editor.is_singleton(cx)
5104 || !editor
5105 .scrollbar_marker_state
5106 .should_refresh(scrollbar_layout.hitbox.size)
5107 {
5108 return;
5109 }
5110
5111 let scrollbar_layout = scrollbar_layout.clone();
5112 let background_highlights = editor.background_highlights.clone();
5113 let snapshot = layout.position_map.snapshot.clone();
5114 let theme = cx.theme().clone();
5115 let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
5116
5117 editor.scrollbar_marker_state.dirty = false;
5118 editor.scrollbar_marker_state.pending_refresh =
5119 Some(cx.spawn_in(window, |editor, mut cx| async move {
5120 let scrollbar_size = scrollbar_layout.hitbox.size;
5121 let scrollbar_markers = cx
5122 .background_spawn(async move {
5123 let max_point = snapshot.display_snapshot.buffer_snapshot.max_point();
5124 let mut marker_quads = Vec::new();
5125 if scrollbar_settings.git_diff {
5126 let marker_row_ranges =
5127 snapshot.buffer_snapshot.diff_hunks().map(|hunk| {
5128 let start_display_row =
5129 MultiBufferPoint::new(hunk.row_range.start.0, 0)
5130 .to_display_point(&snapshot.display_snapshot)
5131 .row();
5132 let mut end_display_row =
5133 MultiBufferPoint::new(hunk.row_range.end.0, 0)
5134 .to_display_point(&snapshot.display_snapshot)
5135 .row();
5136 if end_display_row != start_display_row {
5137 end_display_row.0 -= 1;
5138 }
5139 let color = match &hunk.status().kind {
5140 DiffHunkStatusKind::Added => {
5141 theme.colors().version_control_added
5142 }
5143 DiffHunkStatusKind::Modified => {
5144 theme.colors().version_control_modified
5145 }
5146 DiffHunkStatusKind::Deleted => {
5147 theme.colors().version_control_deleted
5148 }
5149 };
5150 ColoredRange {
5151 start: start_display_row,
5152 end: end_display_row,
5153 color,
5154 }
5155 });
5156
5157 marker_quads.extend(
5158 scrollbar_layout
5159 .marker_quads_for_ranges(marker_row_ranges, Some(0)),
5160 );
5161 }
5162
5163 for (background_highlight_id, (_, background_ranges)) in
5164 background_highlights.iter()
5165 {
5166 let is_search_highlights = *background_highlight_id
5167 == TypeId::of::<BufferSearchHighlights>();
5168 let is_text_highlights = *background_highlight_id
5169 == TypeId::of::<SelectedTextHighlight>();
5170 let is_symbol_occurrences = *background_highlight_id
5171 == TypeId::of::<DocumentHighlightRead>()
5172 || *background_highlight_id
5173 == TypeId::of::<DocumentHighlightWrite>();
5174 if (is_search_highlights && scrollbar_settings.search_results)
5175 || (is_text_highlights && scrollbar_settings.selected_text)
5176 || (is_symbol_occurrences && scrollbar_settings.selected_symbol)
5177 {
5178 let mut color = theme.status().info;
5179 if is_symbol_occurrences {
5180 color.fade_out(0.5);
5181 }
5182 let marker_row_ranges = background_ranges.iter().map(|range| {
5183 let display_start = range
5184 .start
5185 .to_display_point(&snapshot.display_snapshot);
5186 let display_end =
5187 range.end.to_display_point(&snapshot.display_snapshot);
5188 ColoredRange {
5189 start: display_start.row(),
5190 end: display_end.row(),
5191 color,
5192 }
5193 });
5194 marker_quads.extend(
5195 scrollbar_layout
5196 .marker_quads_for_ranges(marker_row_ranges, Some(1)),
5197 );
5198 }
5199 }
5200
5201 if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
5202 let diagnostics = snapshot
5203 .buffer_snapshot
5204 .diagnostics_in_range::<Point>(Point::zero()..max_point)
5205 // Don't show diagnostics the user doesn't care about
5206 .filter(|diagnostic| {
5207 match (
5208 scrollbar_settings.diagnostics,
5209 diagnostic.diagnostic.severity,
5210 ) {
5211 (ScrollbarDiagnostics::All, _) => true,
5212 (
5213 ScrollbarDiagnostics::Error,
5214 DiagnosticSeverity::ERROR,
5215 ) => true,
5216 (
5217 ScrollbarDiagnostics::Warning,
5218 DiagnosticSeverity::ERROR
5219 | DiagnosticSeverity::WARNING,
5220 ) => true,
5221 (
5222 ScrollbarDiagnostics::Information,
5223 DiagnosticSeverity::ERROR
5224 | DiagnosticSeverity::WARNING
5225 | DiagnosticSeverity::INFORMATION,
5226 ) => true,
5227 (_, _) => false,
5228 }
5229 })
5230 // We want to sort by severity, in order to paint the most severe diagnostics last.
5231 .sorted_by_key(|diagnostic| {
5232 std::cmp::Reverse(diagnostic.diagnostic.severity)
5233 });
5234
5235 let marker_row_ranges = diagnostics.into_iter().map(|diagnostic| {
5236 let start_display = diagnostic
5237 .range
5238 .start
5239 .to_display_point(&snapshot.display_snapshot);
5240 let end_display = diagnostic
5241 .range
5242 .end
5243 .to_display_point(&snapshot.display_snapshot);
5244 let color = match diagnostic.diagnostic.severity {
5245 DiagnosticSeverity::ERROR => theme.status().error,
5246 DiagnosticSeverity::WARNING => theme.status().warning,
5247 DiagnosticSeverity::INFORMATION => theme.status().info,
5248 _ => theme.status().hint,
5249 };
5250 ColoredRange {
5251 start: start_display.row(),
5252 end: end_display.row(),
5253 color,
5254 }
5255 });
5256 marker_quads.extend(
5257 scrollbar_layout
5258 .marker_quads_for_ranges(marker_row_ranges, Some(2)),
5259 );
5260 }
5261
5262 Arc::from(marker_quads)
5263 })
5264 .await;
5265
5266 editor.update(&mut cx, |editor, cx| {
5267 editor.scrollbar_marker_state.markers = scrollbar_markers;
5268 editor.scrollbar_marker_state.scrollbar_size = scrollbar_size;
5269 editor.scrollbar_marker_state.pending_refresh = None;
5270 cx.notify();
5271 })?;
5272
5273 Ok(())
5274 }));
5275 });
5276 }
5277
5278 #[allow(clippy::too_many_arguments)]
5279 fn paint_highlighted_range(
5280 &self,
5281 range: Range<DisplayPoint>,
5282 color: Hsla,
5283 corner_radius: Pixels,
5284 line_end_overshoot: Pixels,
5285 layout: &EditorLayout,
5286 window: &mut Window,
5287 ) {
5288 let start_row = layout.visible_display_row_range.start;
5289 let end_row = layout.visible_display_row_range.end;
5290 if range.start != range.end {
5291 let row_range = if range.end.column() == 0 {
5292 cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
5293 } else {
5294 cmp::max(range.start.row(), start_row)
5295 ..cmp::min(range.end.row().next_row(), end_row)
5296 };
5297
5298 let highlighted_range = HighlightedRange {
5299 color,
5300 line_height: layout.position_map.line_height,
5301 corner_radius,
5302 start_y: layout.content_origin.y
5303 + row_range.start.as_f32() * layout.position_map.line_height
5304 - layout.position_map.scroll_pixel_position.y,
5305 lines: row_range
5306 .iter_rows()
5307 .map(|row| {
5308 let line_layout =
5309 &layout.position_map.line_layouts[row.minus(start_row) as usize];
5310 HighlightedRangeLine {
5311 start_x: if row == range.start.row() {
5312 layout.content_origin.x
5313 + line_layout.x_for_index(range.start.column() as usize)
5314 - layout.position_map.scroll_pixel_position.x
5315 } else {
5316 layout.content_origin.x
5317 - layout.position_map.scroll_pixel_position.x
5318 },
5319 end_x: if row == range.end.row() {
5320 layout.content_origin.x
5321 + line_layout.x_for_index(range.end.column() as usize)
5322 - layout.position_map.scroll_pixel_position.x
5323 } else {
5324 layout.content_origin.x + line_layout.width + line_end_overshoot
5325 - layout.position_map.scroll_pixel_position.x
5326 },
5327 }
5328 })
5329 .collect(),
5330 };
5331
5332 highlighted_range.paint(layout.position_map.text_hitbox.bounds, window);
5333 }
5334 }
5335
5336 fn paint_inline_diagnostics(
5337 &mut self,
5338 layout: &mut EditorLayout,
5339 window: &mut Window,
5340 cx: &mut App,
5341 ) {
5342 for mut inline_diagnostic in layout.inline_diagnostics.drain() {
5343 inline_diagnostic.1.paint(window, cx);
5344 }
5345 }
5346
5347 fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
5348 if let Some(mut inline_blame) = layout.inline_blame.take() {
5349 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
5350 inline_blame.paint(window, cx);
5351 })
5352 }
5353 }
5354
5355 fn paint_diff_hunk_controls(
5356 &mut self,
5357 layout: &mut EditorLayout,
5358 window: &mut Window,
5359 cx: &mut App,
5360 ) {
5361 for mut diff_hunk_control in layout.diff_hunk_controls.drain(..) {
5362 diff_hunk_control.paint(window, cx);
5363 }
5364 }
5365
5366 fn paint_blocks(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
5367 for mut block in layout.blocks.drain(..) {
5368 block.element.paint(window, cx);
5369 }
5370 }
5371
5372 fn paint_inline_completion_popover(
5373 &mut self,
5374 layout: &mut EditorLayout,
5375 window: &mut Window,
5376 cx: &mut App,
5377 ) {
5378 if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() {
5379 inline_completion_popover.paint(window, cx);
5380 }
5381 }
5382
5383 fn paint_mouse_context_menu(
5384 &mut self,
5385 layout: &mut EditorLayout,
5386 window: &mut Window,
5387 cx: &mut App,
5388 ) {
5389 if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
5390 mouse_context_menu.paint(window, cx);
5391 }
5392 }
5393
5394 fn paint_scroll_wheel_listener(
5395 &mut self,
5396 layout: &EditorLayout,
5397 window: &mut Window,
5398 cx: &mut App,
5399 ) {
5400 window.on_mouse_event({
5401 let position_map = layout.position_map.clone();
5402 let editor = self.editor.clone();
5403 let hitbox = layout.hitbox.clone();
5404 let mut delta = ScrollDelta::default();
5405
5406 // Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't
5407 // accidentally turn off their scrolling.
5408 let scroll_sensitivity = EditorSettings::get_global(cx).scroll_sensitivity.max(0.01);
5409
5410 move |event: &ScrollWheelEvent, phase, window, cx| {
5411 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
5412 delta = delta.coalesce(event.delta);
5413 editor.update(cx, |editor, cx| {
5414 let position_map: &PositionMap = &position_map;
5415
5416 let line_height = position_map.line_height;
5417 let max_glyph_width = position_map.em_width;
5418 let (delta, axis) = match delta {
5419 gpui::ScrollDelta::Pixels(mut pixels) => {
5420 //Trackpad
5421 let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
5422 (pixels, axis)
5423 }
5424
5425 gpui::ScrollDelta::Lines(lines) => {
5426 //Not trackpad
5427 let pixels =
5428 point(lines.x * max_glyph_width, lines.y * line_height);
5429 (pixels, None)
5430 }
5431 };
5432
5433 let current_scroll_position = position_map.snapshot.scroll_position();
5434 let x = (current_scroll_position.x * max_glyph_width
5435 - (delta.x * scroll_sensitivity))
5436 / max_glyph_width;
5437 let y = (current_scroll_position.y * line_height
5438 - (delta.y * scroll_sensitivity))
5439 / line_height;
5440 let mut scroll_position =
5441 point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
5442 let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll();
5443 if forbid_vertical_scroll {
5444 scroll_position.y = current_scroll_position.y;
5445 }
5446
5447 if scroll_position != current_scroll_position {
5448 editor.scroll(scroll_position, axis, window, cx);
5449 cx.stop_propagation();
5450 } else if y < 0. {
5451 // Due to clamping, we may fail to detect cases of overscroll to the top;
5452 // We want the scroll manager to get an update in such cases and detect the change of direction
5453 // on the next frame.
5454 cx.notify();
5455 }
5456 });
5457 }
5458 }
5459 });
5460 }
5461
5462 fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
5463 self.paint_scroll_wheel_listener(layout, window, cx);
5464
5465 window.on_mouse_event({
5466 let position_map = layout.position_map.clone();
5467 let editor = self.editor.clone();
5468 let diff_hunk_range =
5469 layout
5470 .display_hunks
5471 .iter()
5472 .find_map(|(hunk, hunk_hitbox)| match hunk {
5473 DisplayDiffHunk::Folded { .. } => None,
5474 DisplayDiffHunk::Unfolded {
5475 multi_buffer_range, ..
5476 } => {
5477 if hunk_hitbox
5478 .as_ref()
5479 .map(|hitbox| hitbox.is_hovered(window))
5480 .unwrap_or(false)
5481 {
5482 Some(multi_buffer_range.clone())
5483 } else {
5484 None
5485 }
5486 }
5487 });
5488 let line_numbers = layout.line_numbers.clone();
5489
5490 move |event: &MouseDownEvent, phase, window, cx| {
5491 if phase == DispatchPhase::Bubble {
5492 match event.button {
5493 MouseButton::Left => editor.update(cx, |editor, cx| {
5494 let pending_mouse_down = editor
5495 .pending_mouse_down
5496 .get_or_insert_with(Default::default)
5497 .clone();
5498
5499 *pending_mouse_down.borrow_mut() = Some(event.clone());
5500
5501 Self::mouse_left_down(
5502 editor,
5503 event,
5504 diff_hunk_range.clone(),
5505 &position_map,
5506 line_numbers.as_ref(),
5507 window,
5508 cx,
5509 );
5510 }),
5511 MouseButton::Right => editor.update(cx, |editor, cx| {
5512 Self::mouse_right_down(editor, event, &position_map, window, cx);
5513 }),
5514 MouseButton::Middle => editor.update(cx, |editor, cx| {
5515 Self::mouse_middle_down(editor, event, &position_map, window, cx);
5516 }),
5517 _ => {}
5518 };
5519 }
5520 }
5521 });
5522
5523 window.on_mouse_event({
5524 let editor = self.editor.clone();
5525 let position_map = layout.position_map.clone();
5526
5527 move |event: &MouseUpEvent, phase, window, cx| {
5528 if phase == DispatchPhase::Bubble {
5529 editor.update(cx, |editor, cx| {
5530 Self::mouse_up(editor, event, &position_map, window, cx)
5531 });
5532 }
5533 }
5534 });
5535
5536 window.on_mouse_event({
5537 let editor = self.editor.clone();
5538 let position_map = layout.position_map.clone();
5539 let mut captured_mouse_down = None;
5540
5541 move |event: &MouseUpEvent, phase, window, cx| match phase {
5542 // Clear the pending mouse down during the capture phase,
5543 // so that it happens even if another event handler stops
5544 // propagation.
5545 DispatchPhase::Capture => editor.update(cx, |editor, _cx| {
5546 let pending_mouse_down = editor
5547 .pending_mouse_down
5548 .get_or_insert_with(Default::default)
5549 .clone();
5550
5551 let mut pending_mouse_down = pending_mouse_down.borrow_mut();
5552 if pending_mouse_down.is_some() && position_map.text_hitbox.is_hovered(window) {
5553 captured_mouse_down = pending_mouse_down.take();
5554 window.refresh();
5555 }
5556 }),
5557 // Fire click handlers during the bubble phase.
5558 DispatchPhase::Bubble => editor.update(cx, |editor, cx| {
5559 if let Some(mouse_down) = captured_mouse_down.take() {
5560 let event = ClickEvent {
5561 down: mouse_down,
5562 up: event.clone(),
5563 };
5564 Self::click(editor, &event, &position_map, window, cx);
5565 }
5566 }),
5567 }
5568 });
5569
5570 window.on_mouse_event({
5571 let position_map = layout.position_map.clone();
5572 let editor = self.editor.clone();
5573
5574 move |event: &MouseMoveEvent, phase, window, cx| {
5575 if phase == DispatchPhase::Bubble {
5576 editor.update(cx, |editor, cx| {
5577 if editor.hover_state.focused(window, cx) {
5578 return;
5579 }
5580 if event.pressed_button == Some(MouseButton::Left)
5581 || event.pressed_button == Some(MouseButton::Middle)
5582 {
5583 Self::mouse_dragged(editor, event, &position_map, window, cx)
5584 }
5585
5586 Self::mouse_moved(editor, event, &position_map, window, cx)
5587 });
5588 }
5589 }
5590 });
5591 }
5592
5593 fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels {
5594 bounds.top_right().x - self.style.scrollbar_width
5595 }
5596
5597 fn column_pixels(&self, column: usize, window: &mut Window, _: &mut App) -> Pixels {
5598 let style = &self.style;
5599 let font_size = style.text.font_size.to_pixels(window.rem_size());
5600 let layout = window
5601 .text_system()
5602 .shape_line(
5603 SharedString::from(" ".repeat(column)),
5604 font_size,
5605 &[TextRun {
5606 len: column,
5607 font: style.text.font(),
5608 color: Hsla::default(),
5609 background_color: None,
5610 underline: None,
5611 strikethrough: None,
5612 }],
5613 )
5614 .unwrap();
5615
5616 layout.width
5617 }
5618
5619 fn max_line_number_width(
5620 &self,
5621 snapshot: &EditorSnapshot,
5622 window: &mut Window,
5623 cx: &mut App,
5624 ) -> Pixels {
5625 let digit_count = (snapshot.widest_line_number() as f32).log10().floor() as usize + 1;
5626 self.column_pixels(digit_count, window, cx)
5627 }
5628
5629 fn shape_line_number(
5630 &self,
5631 text: SharedString,
5632 color: Hsla,
5633 window: &mut Window,
5634 ) -> anyhow::Result<ShapedLine> {
5635 let run = TextRun {
5636 len: text.len(),
5637 font: self.style.text.font(),
5638 color,
5639 background_color: None,
5640 underline: None,
5641 strikethrough: None,
5642 };
5643 window.text_system().shape_line(
5644 text,
5645 self.style.text.font_size.to_pixels(window.rem_size()),
5646 &[run],
5647 )
5648 }
5649}
5650
5651fn header_jump_data(
5652 snapshot: &EditorSnapshot,
5653 block_row_start: DisplayRow,
5654 height: u32,
5655 for_excerpt: &ExcerptInfo,
5656) -> JumpData {
5657 let range = &for_excerpt.range;
5658 let buffer = &for_excerpt.buffer;
5659 let jump_anchor = range
5660 .primary
5661 .as_ref()
5662 .map_or(range.context.start, |primary| primary.start);
5663
5664 let excerpt_start = range.context.start;
5665 let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
5666 let rows_from_excerpt_start = if jump_anchor == excerpt_start {
5667 0
5668 } else {
5669 let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
5670 jump_position.row.saturating_sub(excerpt_start_point.row)
5671 };
5672
5673 let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start)
5674 .saturating_sub(
5675 snapshot
5676 .scroll_anchor
5677 .scroll_position(&snapshot.display_snapshot)
5678 .y as u32,
5679 );
5680
5681 JumpData::MultiBufferPoint {
5682 excerpt_id: for_excerpt.id,
5683 anchor: jump_anchor,
5684 position: jump_position,
5685 line_offset_from_top,
5686 }
5687}
5688
5689pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
5690
5691impl AcceptEditPredictionBinding {
5692 pub fn keystroke(&self) -> Option<&Keystroke> {
5693 if let Some(binding) = self.0.as_ref() {
5694 match &binding.keystrokes() {
5695 [keystroke] => Some(keystroke),
5696 _ => None,
5697 }
5698 } else {
5699 None
5700 }
5701 }
5702}
5703
5704#[allow(clippy::too_many_arguments)]
5705fn prepaint_gutter_button(
5706 button: IconButton,
5707 row: DisplayRow,
5708 line_height: Pixels,
5709 gutter_dimensions: &GutterDimensions,
5710 scroll_pixel_position: gpui::Point<Pixels>,
5711 gutter_hitbox: &Hitbox,
5712 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
5713 window: &mut Window,
5714 cx: &mut App,
5715) -> AnyElement {
5716 let mut button = button.into_any_element();
5717 let available_space = size(
5718 AvailableSpace::MinContent,
5719 AvailableSpace::Definite(line_height),
5720 );
5721 let indicator_size = button.layout_as_root(available_space, window, cx);
5722
5723 let blame_width = gutter_dimensions.git_blame_entries_width;
5724 let gutter_width = display_hunks
5725 .binary_search_by(|(hunk, _)| match hunk {
5726 DisplayDiffHunk::Folded { display_row } => display_row.cmp(&row),
5727 DisplayDiffHunk::Unfolded {
5728 display_row_range, ..
5729 } => {
5730 if display_row_range.end <= row {
5731 Ordering::Less
5732 } else if display_row_range.start > row {
5733 Ordering::Greater
5734 } else {
5735 Ordering::Equal
5736 }
5737 }
5738 })
5739 .ok()
5740 .and_then(|ix| Some(display_hunks[ix].1.as_ref()?.size.width));
5741 let left_offset = blame_width.max(gutter_width).unwrap_or_default();
5742
5743 let mut x = left_offset;
5744 let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
5745 - indicator_size.width
5746 - left_offset;
5747 x += available_width / 2.;
5748
5749 let mut y = row.as_f32() * line_height - scroll_pixel_position.y;
5750 y += (line_height - indicator_size.height) / 2.;
5751
5752 button.prepaint_as_root(
5753 gutter_hitbox.origin + point(x, y),
5754 available_space,
5755 window,
5756 cx,
5757 );
5758 button
5759}
5760
5761fn render_inline_blame_entry(
5762 editor: Entity<Editor>,
5763 blame: &gpui::Entity<GitBlame>,
5764 blame_entry: BlameEntry,
5765 style: &EditorStyle,
5766 cx: &mut App,
5767) -> AnyElement {
5768 let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
5769
5770 let author = blame_entry.author.as_deref().unwrap_or_default();
5771 let summary_enabled = ProjectSettings::get_global(cx)
5772 .git
5773 .show_inline_commit_summary();
5774
5775 let text = match blame_entry.summary.as_ref() {
5776 Some(summary) if summary_enabled => {
5777 format!("{}, {} - {}", author, relative_timestamp, summary)
5778 }
5779 _ => format!("{}, {}", author, relative_timestamp),
5780 };
5781 let blame = blame.clone();
5782 let blame_entry = blame_entry.clone();
5783
5784 h_flex()
5785 .id("inline-blame")
5786 .w_full()
5787 .font_family(style.text.font().family)
5788 .text_color(cx.theme().status().hint)
5789 .line_height(style.text.line_height)
5790 .child(Icon::new(IconName::FileGit).color(Color::Hint))
5791 .child(text)
5792 .gap_2()
5793 .hoverable_tooltip(move |window, cx| {
5794 let details = blame.read(cx).details_for_entry(&blame_entry);
5795 let tooltip =
5796 cx.new(|cx| CommitTooltip::blame_entry(&blame_entry, details, window, cx));
5797 editor.update(cx, |editor, _| {
5798 editor.git_blame_inline_tooltip = Some(tooltip.downgrade())
5799 });
5800 tooltip.into()
5801 })
5802 .into_any()
5803}
5804
5805fn render_blame_entry(
5806 ix: usize,
5807 blame: &gpui::Entity<GitBlame>,
5808 blame_entry: BlameEntry,
5809 style: &EditorStyle,
5810 last_used_color: &mut Option<(PlayerColor, Oid)>,
5811 editor: Entity<Editor>,
5812 cx: &mut App,
5813) -> AnyElement {
5814 let mut sha_color = cx
5815 .theme()
5816 .players()
5817 .color_for_participant(blame_entry.sha.into());
5818 // If the last color we used is the same as the one we get for this line, but
5819 // the commit SHAs are different, then we try again to get a different color.
5820 match *last_used_color {
5821 Some((color, sha)) if sha != blame_entry.sha && color.cursor == sha_color.cursor => {
5822 let index: u32 = blame_entry.sha.into();
5823 sha_color = cx.theme().players().color_for_participant(index + 1);
5824 }
5825 _ => {}
5826 };
5827 last_used_color.replace((sha_color, blame_entry.sha));
5828
5829 let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
5830
5831 let short_commit_id = blame_entry.sha.display_short();
5832
5833 let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
5834 let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
5835 let details = blame.read(cx).details_for_entry(&blame_entry);
5836
5837 h_flex()
5838 .w_full()
5839 .justify_between()
5840 .font_family(style.text.font().family)
5841 .line_height(style.text.line_height)
5842 .id(("blame", ix))
5843 .text_color(cx.theme().status().hint)
5844 .pr_2()
5845 .gap_2()
5846 .child(
5847 h_flex()
5848 .items_center()
5849 .gap_2()
5850 .child(div().text_color(sha_color.cursor).child(short_commit_id))
5851 .child(name),
5852 )
5853 .child(relative_timestamp)
5854 .on_mouse_down(MouseButton::Right, {
5855 let blame_entry = blame_entry.clone();
5856 let details = details.clone();
5857 move |event, window, cx| {
5858 deploy_blame_entry_context_menu(
5859 &blame_entry,
5860 details.as_ref(),
5861 editor.clone(),
5862 event.position,
5863 window,
5864 cx,
5865 );
5866 }
5867 })
5868 .hover(|style| style.bg(cx.theme().colors().element_hover))
5869 .when_some(
5870 details
5871 .as_ref()
5872 .and_then(|details| details.permalink.clone()),
5873 |this, url| {
5874 this.cursor_pointer().on_click(move |_, _, cx| {
5875 cx.stop_propagation();
5876 cx.open_url(url.as_str())
5877 })
5878 },
5879 )
5880 .hoverable_tooltip(move |window, cx| {
5881 cx.new(|cx| CommitTooltip::blame_entry(&blame_entry, details.clone(), window, cx))
5882 .into()
5883 })
5884 .into_any()
5885}
5886
5887fn deploy_blame_entry_context_menu(
5888 blame_entry: &BlameEntry,
5889 details: Option<&ParsedCommitMessage>,
5890 editor: Entity<Editor>,
5891 position: gpui::Point<Pixels>,
5892 window: &mut Window,
5893 cx: &mut App,
5894) {
5895 let context_menu = ContextMenu::build(window, cx, move |menu, _, _| {
5896 let sha = format!("{}", blame_entry.sha);
5897 menu.on_blur_subscription(Subscription::new(|| {}))
5898 .entry("Copy commit SHA", None, move |_, cx| {
5899 cx.write_to_clipboard(ClipboardItem::new_string(sha.clone()));
5900 })
5901 .when_some(
5902 details.and_then(|details| details.permalink.clone()),
5903 |this, url| {
5904 this.entry("Open permalink", None, move |_, cx| {
5905 cx.open_url(url.as_str())
5906 })
5907 },
5908 )
5909 });
5910
5911 editor.update(cx, move |editor, cx| {
5912 editor.mouse_context_menu = Some(MouseContextMenu::new(
5913 MenuPosition::PinnedToScreen(position),
5914 context_menu,
5915 window,
5916 cx,
5917 ));
5918 cx.notify();
5919 });
5920}
5921
5922#[derive(Debug)]
5923pub(crate) struct LineWithInvisibles {
5924 fragments: SmallVec<[LineFragment; 1]>,
5925 invisibles: Vec<Invisible>,
5926 len: usize,
5927 pub(crate) width: Pixels,
5928 font_size: Pixels,
5929}
5930
5931#[allow(clippy::large_enum_variant)]
5932enum LineFragment {
5933 Text(ShapedLine),
5934 Element {
5935 element: Option<AnyElement>,
5936 size: Size<Pixels>,
5937 len: usize,
5938 },
5939}
5940
5941impl fmt::Debug for LineFragment {
5942 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5943 match self {
5944 LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
5945 LineFragment::Element { size, len, .. } => f
5946 .debug_struct("Element")
5947 .field("size", size)
5948 .field("len", len)
5949 .finish(),
5950 }
5951 }
5952}
5953
5954impl LineWithInvisibles {
5955 #[allow(clippy::too_many_arguments)]
5956 fn from_chunks<'a>(
5957 chunks: impl Iterator<Item = HighlightedChunk<'a>>,
5958 editor_style: &EditorStyle,
5959 max_line_len: usize,
5960 max_line_count: usize,
5961 editor_mode: EditorMode,
5962 text_width: Pixels,
5963 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
5964 window: &mut Window,
5965 cx: &mut App,
5966 ) -> Vec<Self> {
5967 let text_style = &editor_style.text;
5968 let mut layouts = Vec::with_capacity(max_line_count);
5969 let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
5970 let mut line = String::new();
5971 let mut invisibles = Vec::new();
5972 let mut width = Pixels::ZERO;
5973 let mut len = 0;
5974 let mut styles = Vec::new();
5975 let mut non_whitespace_added = false;
5976 let mut row = 0;
5977 let mut line_exceeded_max_len = false;
5978 let font_size = text_style.font_size.to_pixels(window.rem_size());
5979
5980 let ellipsis = SharedString::from("⋯");
5981
5982 for highlighted_chunk in chunks.chain([HighlightedChunk {
5983 text: "\n",
5984 style: None,
5985 is_tab: false,
5986 replacement: None,
5987 }]) {
5988 if let Some(replacement) = highlighted_chunk.replacement {
5989 if !line.is_empty() {
5990 let shaped_line = window
5991 .text_system()
5992 .shape_line(line.clone().into(), font_size, &styles)
5993 .unwrap();
5994 width += shaped_line.width;
5995 len += shaped_line.len;
5996 fragments.push(LineFragment::Text(shaped_line));
5997 line.clear();
5998 styles.clear();
5999 }
6000
6001 match replacement {
6002 ChunkReplacement::Renderer(renderer) => {
6003 let available_width = if renderer.constrain_width {
6004 let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
6005 ellipsis.clone()
6006 } else {
6007 SharedString::from(Arc::from(highlighted_chunk.text))
6008 };
6009 let shaped_line = window
6010 .text_system()
6011 .shape_line(
6012 chunk,
6013 font_size,
6014 &[text_style.to_run(highlighted_chunk.text.len())],
6015 )
6016 .unwrap();
6017 AvailableSpace::Definite(shaped_line.width)
6018 } else {
6019 AvailableSpace::MinContent
6020 };
6021
6022 let mut element = (renderer.render)(&mut ChunkRendererContext {
6023 context: cx,
6024 window,
6025 max_width: text_width,
6026 });
6027 let line_height = text_style.line_height_in_pixels(window.rem_size());
6028 let size = element.layout_as_root(
6029 size(available_width, AvailableSpace::Definite(line_height)),
6030 window,
6031 cx,
6032 );
6033
6034 width += size.width;
6035 len += highlighted_chunk.text.len();
6036 fragments.push(LineFragment::Element {
6037 element: Some(element),
6038 size,
6039 len: highlighted_chunk.text.len(),
6040 });
6041 }
6042 ChunkReplacement::Str(x) => {
6043 let text_style = if let Some(style) = highlighted_chunk.style {
6044 Cow::Owned(text_style.clone().highlight(style))
6045 } else {
6046 Cow::Borrowed(text_style)
6047 };
6048
6049 let run = TextRun {
6050 len: x.len(),
6051 font: text_style.font(),
6052 color: text_style.color,
6053 background_color: text_style.background_color,
6054 underline: text_style.underline,
6055 strikethrough: text_style.strikethrough,
6056 };
6057 let line_layout = window
6058 .text_system()
6059 .shape_line(x, font_size, &[run])
6060 .unwrap()
6061 .with_len(highlighted_chunk.text.len());
6062
6063 width += line_layout.width;
6064 len += highlighted_chunk.text.len();
6065 fragments.push(LineFragment::Text(line_layout))
6066 }
6067 }
6068 } else {
6069 for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
6070 if ix > 0 {
6071 let shaped_line = window
6072 .text_system()
6073 .shape_line(line.clone().into(), font_size, &styles)
6074 .unwrap();
6075 width += shaped_line.width;
6076 len += shaped_line.len;
6077 fragments.push(LineFragment::Text(shaped_line));
6078 layouts.push(Self {
6079 width: mem::take(&mut width),
6080 len: mem::take(&mut len),
6081 fragments: mem::take(&mut fragments),
6082 invisibles: std::mem::take(&mut invisibles),
6083 font_size,
6084 });
6085
6086 line.clear();
6087 styles.clear();
6088 row += 1;
6089 line_exceeded_max_len = false;
6090 non_whitespace_added = false;
6091 if row == max_line_count {
6092 return layouts;
6093 }
6094 }
6095
6096 if !line_chunk.is_empty() && !line_exceeded_max_len {
6097 let text_style = if let Some(style) = highlighted_chunk.style {
6098 Cow::Owned(text_style.clone().highlight(style))
6099 } else {
6100 Cow::Borrowed(text_style)
6101 };
6102
6103 if line.len() + line_chunk.len() > max_line_len {
6104 let mut chunk_len = max_line_len - line.len();
6105 while !line_chunk.is_char_boundary(chunk_len) {
6106 chunk_len -= 1;
6107 }
6108 line_chunk = &line_chunk[..chunk_len];
6109 line_exceeded_max_len = true;
6110 }
6111
6112 styles.push(TextRun {
6113 len: line_chunk.len(),
6114 font: text_style.font(),
6115 color: text_style.color,
6116 background_color: text_style.background_color,
6117 underline: text_style.underline,
6118 strikethrough: text_style.strikethrough,
6119 });
6120
6121 if editor_mode == EditorMode::Full {
6122 // Line wrap pads its contents with fake whitespaces,
6123 // avoid printing them
6124 let is_soft_wrapped = is_row_soft_wrapped(row);
6125 if highlighted_chunk.is_tab {
6126 if non_whitespace_added || !is_soft_wrapped {
6127 invisibles.push(Invisible::Tab {
6128 line_start_offset: line.len(),
6129 line_end_offset: line.len() + line_chunk.len(),
6130 });
6131 }
6132 } else {
6133 invisibles.extend(line_chunk.char_indices().filter_map(
6134 |(index, c)| {
6135 let is_whitespace = c.is_whitespace();
6136 non_whitespace_added |= !is_whitespace;
6137 if is_whitespace
6138 && (non_whitespace_added || !is_soft_wrapped)
6139 {
6140 Some(Invisible::Whitespace {
6141 line_offset: line.len() + index,
6142 })
6143 } else {
6144 None
6145 }
6146 },
6147 ))
6148 }
6149 }
6150
6151 line.push_str(line_chunk);
6152 }
6153 }
6154 }
6155 }
6156
6157 layouts
6158 }
6159
6160 #[allow(clippy::too_many_arguments)]
6161 fn prepaint(
6162 &mut self,
6163 line_height: Pixels,
6164 scroll_pixel_position: gpui::Point<Pixels>,
6165 row: DisplayRow,
6166 content_origin: gpui::Point<Pixels>,
6167 line_elements: &mut SmallVec<[AnyElement; 1]>,
6168 window: &mut Window,
6169 cx: &mut App,
6170 ) {
6171 let line_y = line_height * (row.as_f32() - scroll_pixel_position.y / line_height);
6172 let mut fragment_origin = content_origin + gpui::point(-scroll_pixel_position.x, line_y);
6173 for fragment in &mut self.fragments {
6174 match fragment {
6175 LineFragment::Text(line) => {
6176 fragment_origin.x += line.width;
6177 }
6178 LineFragment::Element { element, size, .. } => {
6179 let mut element = element
6180 .take()
6181 .expect("you can't prepaint LineWithInvisibles twice");
6182
6183 // Center the element vertically within the line.
6184 let mut element_origin = fragment_origin;
6185 element_origin.y += (line_height - size.height) / 2.;
6186 element.prepaint_at(element_origin, window, cx);
6187 line_elements.push(element);
6188
6189 fragment_origin.x += size.width;
6190 }
6191 }
6192 }
6193 }
6194
6195 #[allow(clippy::too_many_arguments)]
6196 fn draw(
6197 &self,
6198 layout: &EditorLayout,
6199 row: DisplayRow,
6200 content_origin: gpui::Point<Pixels>,
6201 whitespace_setting: ShowWhitespaceSetting,
6202 selection_ranges: &[Range<DisplayPoint>],
6203 window: &mut Window,
6204 cx: &mut App,
6205 ) {
6206 let line_height = layout.position_map.line_height;
6207 let line_y = line_height
6208 * (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
6209
6210 let mut fragment_origin =
6211 content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
6212
6213 for fragment in &self.fragments {
6214 match fragment {
6215 LineFragment::Text(line) => {
6216 line.paint(fragment_origin, line_height, window, cx)
6217 .log_err();
6218 fragment_origin.x += line.width;
6219 }
6220 LineFragment::Element { size, .. } => {
6221 fragment_origin.x += size.width;
6222 }
6223 }
6224 }
6225
6226 self.draw_invisibles(
6227 selection_ranges,
6228 layout,
6229 content_origin,
6230 line_y,
6231 row,
6232 line_height,
6233 whitespace_setting,
6234 window,
6235 cx,
6236 );
6237 }
6238
6239 #[allow(clippy::too_many_arguments)]
6240 fn draw_invisibles(
6241 &self,
6242 selection_ranges: &[Range<DisplayPoint>],
6243 layout: &EditorLayout,
6244 content_origin: gpui::Point<Pixels>,
6245 line_y: Pixels,
6246 row: DisplayRow,
6247 line_height: Pixels,
6248 whitespace_setting: ShowWhitespaceSetting,
6249 window: &mut Window,
6250 cx: &mut App,
6251 ) {
6252 let extract_whitespace_info = |invisible: &Invisible| {
6253 let (token_offset, token_end_offset, invisible_symbol) = match invisible {
6254 Invisible::Tab {
6255 line_start_offset,
6256 line_end_offset,
6257 } => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
6258 Invisible::Whitespace { line_offset } => {
6259 (*line_offset, line_offset + 1, &layout.space_invisible)
6260 }
6261 };
6262
6263 let x_offset = self.x_for_index(token_offset);
6264 let invisible_offset =
6265 (layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0;
6266 let origin = content_origin
6267 + gpui::point(
6268 x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x,
6269 line_y,
6270 );
6271
6272 (
6273 [token_offset, token_end_offset],
6274 Box::new(move |window: &mut Window, cx: &mut App| {
6275 invisible_symbol
6276 .paint(origin, line_height, window, cx)
6277 .log_err();
6278 }),
6279 )
6280 };
6281
6282 let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
6283 match whitespace_setting {
6284 ShowWhitespaceSetting::None => (),
6285 ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(window, cx)),
6286 ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
6287 let invisible_point = DisplayPoint::new(row, start as u32);
6288 if !selection_ranges
6289 .iter()
6290 .any(|region| region.start <= invisible_point && invisible_point < region.end)
6291 {
6292 return;
6293 }
6294
6295 paint(window, cx);
6296 }),
6297
6298 // For a whitespace to be on a boundary, any of the following conditions need to be met:
6299 // - It is a tab
6300 // - It is adjacent to an edge (start or end)
6301 // - It is adjacent to a whitespace (left or right)
6302 ShowWhitespaceSetting::Boundary => {
6303 // 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
6304 // the above cases.
6305 // Note: We zip in the original `invisibles` to check for tab equality
6306 let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut Window, &mut App)>)> = None;
6307 for (([start, end], paint), invisible) in
6308 invisible_iter.zip_eq(self.invisibles.iter())
6309 {
6310 let should_render = match (&last_seen, invisible) {
6311 (_, Invisible::Tab { .. }) => true,
6312 (Some((_, last_end, _)), _) => *last_end == start,
6313 _ => false,
6314 };
6315
6316 if should_render || start == 0 || end == self.len {
6317 paint(window, cx);
6318
6319 // Since we are scanning from the left, we will skip over the first available whitespace that is part
6320 // of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
6321 if let Some((should_render_last, last_end, paint_last)) = last_seen {
6322 // Note that we need to make sure that the last one is actually adjacent
6323 if !should_render_last && last_end == start {
6324 paint_last(window, cx);
6325 }
6326 }
6327 }
6328
6329 // Manually render anything within a selection
6330 let invisible_point = DisplayPoint::new(row, start as u32);
6331 if selection_ranges.iter().any(|region| {
6332 region.start <= invisible_point && invisible_point < region.end
6333 }) {
6334 paint(window, cx);
6335 }
6336
6337 last_seen = Some((should_render, end, paint));
6338 }
6339 }
6340 }
6341 }
6342
6343 pub fn x_for_index(&self, index: usize) -> Pixels {
6344 let mut fragment_start_x = Pixels::ZERO;
6345 let mut fragment_start_index = 0;
6346
6347 for fragment in &self.fragments {
6348 match fragment {
6349 LineFragment::Text(shaped_line) => {
6350 let fragment_end_index = fragment_start_index + shaped_line.len;
6351 if index < fragment_end_index {
6352 return fragment_start_x
6353 + shaped_line.x_for_index(index - fragment_start_index);
6354 }
6355 fragment_start_x += shaped_line.width;
6356 fragment_start_index = fragment_end_index;
6357 }
6358 LineFragment::Element { len, size, .. } => {
6359 let fragment_end_index = fragment_start_index + len;
6360 if index < fragment_end_index {
6361 return fragment_start_x;
6362 }
6363 fragment_start_x += size.width;
6364 fragment_start_index = fragment_end_index;
6365 }
6366 }
6367 }
6368
6369 fragment_start_x
6370 }
6371
6372 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
6373 let mut fragment_start_x = Pixels::ZERO;
6374 let mut fragment_start_index = 0;
6375
6376 for fragment in &self.fragments {
6377 match fragment {
6378 LineFragment::Text(shaped_line) => {
6379 let fragment_end_x = fragment_start_x + shaped_line.width;
6380 if x < fragment_end_x {
6381 return Some(
6382 fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
6383 );
6384 }
6385 fragment_start_x = fragment_end_x;
6386 fragment_start_index += shaped_line.len;
6387 }
6388 LineFragment::Element { len, size, .. } => {
6389 let fragment_end_x = fragment_start_x + size.width;
6390 if x < fragment_end_x {
6391 return Some(fragment_start_index);
6392 }
6393 fragment_start_index += len;
6394 fragment_start_x = fragment_end_x;
6395 }
6396 }
6397 }
6398
6399 None
6400 }
6401
6402 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
6403 let mut fragment_start_index = 0;
6404
6405 for fragment in &self.fragments {
6406 match fragment {
6407 LineFragment::Text(shaped_line) => {
6408 let fragment_end_index = fragment_start_index + shaped_line.len;
6409 if index < fragment_end_index {
6410 return shaped_line.font_id_for_index(index - fragment_start_index);
6411 }
6412 fragment_start_index = fragment_end_index;
6413 }
6414 LineFragment::Element { len, .. } => {
6415 let fragment_end_index = fragment_start_index + len;
6416 if index < fragment_end_index {
6417 return None;
6418 }
6419 fragment_start_index = fragment_end_index;
6420 }
6421 }
6422 }
6423
6424 None
6425 }
6426}
6427
6428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6429enum Invisible {
6430 /// A tab character
6431 ///
6432 /// A tab character is internally represented by spaces (configured by the user's tab width)
6433 /// aligned to the nearest column, so it's necessary to store the start and end offset for
6434 /// adjacency checks.
6435 Tab {
6436 line_start_offset: usize,
6437 line_end_offset: usize,
6438 },
6439 Whitespace {
6440 line_offset: usize,
6441 },
6442}
6443
6444impl EditorElement {
6445 /// Returns the rem size to use when rendering the [`EditorElement`].
6446 ///
6447 /// This allows UI elements to scale based on the `buffer_font_size`.
6448 fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
6449 match self.editor.read(cx).mode {
6450 EditorMode::Full => {
6451 let buffer_font_size = self.style.text.font_size;
6452 match buffer_font_size {
6453 AbsoluteLength::Pixels(pixels) => {
6454 let rem_size_scale = {
6455 // Our default UI font size is 14px on a 16px base scale.
6456 // This means the default UI font size is 0.875rems.
6457 let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
6458
6459 // We then determine the delta between a single rem and the default font
6460 // size scale.
6461 let default_font_size_delta = 1. - default_font_size_scale;
6462
6463 // Finally, we add this delta to 1rem to get the scale factor that
6464 // should be used to scale up the UI.
6465 1. + default_font_size_delta
6466 };
6467
6468 Some(pixels * rem_size_scale)
6469 }
6470 AbsoluteLength::Rems(rems) => {
6471 Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into()))
6472 }
6473 }
6474 }
6475 // We currently use single-line and auto-height editors in UI contexts,
6476 // so we don't want to scale everything with the buffer font size, as it
6477 // ends up looking off.
6478 EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => None,
6479 }
6480 }
6481}
6482
6483impl Element for EditorElement {
6484 type RequestLayoutState = ();
6485 type PrepaintState = EditorLayout;
6486
6487 fn id(&self) -> Option<ElementId> {
6488 None
6489 }
6490
6491 fn request_layout(
6492 &mut self,
6493 _: Option<&GlobalElementId>,
6494 window: &mut Window,
6495 cx: &mut App,
6496 ) -> (gpui::LayoutId, ()) {
6497 let rem_size = self.rem_size(cx);
6498 window.with_rem_size(rem_size, |window| {
6499 self.editor.update(cx, |editor, cx| {
6500 editor.set_style(self.style.clone(), window, cx);
6501
6502 let layout_id = match editor.mode {
6503 EditorMode::SingleLine { auto_width } => {
6504 let rem_size = window.rem_size();
6505
6506 let height = self.style.text.line_height_in_pixels(rem_size);
6507 if auto_width {
6508 let editor_handle = cx.entity().clone();
6509 let style = self.style.clone();
6510 window.request_measured_layout(
6511 Style::default(),
6512 move |_, _, window, cx| {
6513 let editor_snapshot = editor_handle
6514 .update(cx, |editor, cx| editor.snapshot(window, cx));
6515 let line = Self::layout_lines(
6516 DisplayRow(0)..DisplayRow(1),
6517 &editor_snapshot,
6518 &style,
6519 px(f32::MAX),
6520 |_| false, // Single lines never soft wrap
6521 window,
6522 cx,
6523 )
6524 .pop()
6525 .unwrap();
6526
6527 let font_id =
6528 window.text_system().resolve_font(&style.text.font());
6529 let font_size =
6530 style.text.font_size.to_pixels(window.rem_size());
6531 let em_width =
6532 window.text_system().em_width(font_id, font_size).unwrap();
6533
6534 size(line.width + em_width, height)
6535 },
6536 )
6537 } else {
6538 let mut style = Style::default();
6539 style.size.height = height.into();
6540 style.size.width = relative(1.).into();
6541 window.request_layout(style, None, cx)
6542 }
6543 }
6544 EditorMode::AutoHeight { max_lines } => {
6545 let editor_handle = cx.entity().clone();
6546 let max_line_number_width =
6547 self.max_line_number_width(&editor.snapshot(window, cx), window, cx);
6548 window.request_measured_layout(
6549 Style::default(),
6550 move |known_dimensions, available_space, window, cx| {
6551 editor_handle
6552 .update(cx, |editor, cx| {
6553 compute_auto_height_layout(
6554 editor,
6555 max_lines,
6556 max_line_number_width,
6557 known_dimensions,
6558 available_space.width,
6559 window,
6560 cx,
6561 )
6562 })
6563 .unwrap_or_default()
6564 },
6565 )
6566 }
6567 EditorMode::Full => {
6568 let mut style = Style::default();
6569 style.size.width = relative(1.).into();
6570 style.size.height = relative(1.).into();
6571 window.request_layout(style, None, cx)
6572 }
6573 };
6574
6575 (layout_id, ())
6576 })
6577 })
6578 }
6579
6580 fn prepaint(
6581 &mut self,
6582 _: Option<&GlobalElementId>,
6583 bounds: Bounds<Pixels>,
6584 _: &mut Self::RequestLayoutState,
6585 window: &mut Window,
6586 cx: &mut App,
6587 ) -> Self::PrepaintState {
6588 let text_style = TextStyleRefinement {
6589 font_size: Some(self.style.text.font_size),
6590 line_height: Some(self.style.text.line_height),
6591 ..Default::default()
6592 };
6593 let focus_handle = self.editor.focus_handle(cx);
6594 window.set_view_id(self.editor.entity_id());
6595 window.set_focus_handle(&focus_handle, cx);
6596
6597 let rem_size = self.rem_size(cx);
6598 window.with_rem_size(rem_size, |window| {
6599 window.with_text_style(Some(text_style), |window| {
6600 window.with_content_mask(Some(ContentMask { bounds }), |window| {
6601 let mut snapshot = self
6602 .editor
6603 .update(cx, |editor, cx| editor.snapshot(window, cx));
6604 let style = self.style.clone();
6605
6606 let font_id = window.text_system().resolve_font(&style.text.font());
6607 let font_size = style.text.font_size.to_pixels(window.rem_size());
6608 let line_height = style.text.line_height_in_pixels(window.rem_size());
6609 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
6610 let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
6611
6612 let letter_size = size(em_width, line_height);
6613
6614 let gutter_dimensions = snapshot
6615 .gutter_dimensions(
6616 font_id,
6617 font_size,
6618 self.max_line_number_width(&snapshot, window, cx),
6619 cx,
6620 )
6621 .unwrap_or_default();
6622 let text_width = bounds.size.width - gutter_dimensions.width;
6623
6624 let editor_width =
6625 text_width - gutter_dimensions.margin - em_width - style.scrollbar_width;
6626
6627 snapshot = self.editor.update(cx, |editor, cx| {
6628 editor.last_bounds = Some(bounds);
6629 editor.gutter_dimensions = gutter_dimensions;
6630 editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
6631
6632 if matches!(editor.mode, EditorMode::AutoHeight { .. }) {
6633 snapshot
6634 } else {
6635 let wrap_width = match editor.soft_wrap_mode(cx) {
6636 SoftWrap::GitDiff => None,
6637 SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
6638 SoftWrap::EditorWidth => Some(editor_width),
6639 SoftWrap::Column(column) => Some(column as f32 * em_advance),
6640 SoftWrap::Bounded(column) => {
6641 Some(editor_width.min(column as f32 * em_advance))
6642 }
6643 };
6644
6645 if editor.set_wrap_width(wrap_width, cx) {
6646 editor.snapshot(window, cx)
6647 } else {
6648 snapshot
6649 }
6650 }
6651 });
6652
6653 let wrap_guides = self
6654 .editor
6655 .read(cx)
6656 .wrap_guides(cx)
6657 .iter()
6658 .map(|(guide, active)| (self.column_pixels(*guide, window, cx), *active))
6659 .collect::<SmallVec<[_; 2]>>();
6660
6661 let hitbox = window.insert_hitbox(bounds, false);
6662 let gutter_hitbox =
6663 window.insert_hitbox(gutter_bounds(bounds, gutter_dimensions), false);
6664 let text_hitbox = window.insert_hitbox(
6665 Bounds {
6666 origin: gutter_hitbox.top_right(),
6667 size: size(text_width, bounds.size.height),
6668 },
6669 false,
6670 );
6671 // Offset the content_bounds from the text_bounds by the gutter margin (which
6672 // is roughly half a character wide) to make hit testing work more like how we want.
6673 let content_origin =
6674 text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO);
6675
6676 let scrollbar_bounds =
6677 Bounds::from_corners(content_origin, bounds.bottom_right());
6678
6679 let height_in_lines = scrollbar_bounds.size.height / line_height;
6680
6681 // NOTE: The max row number in the current file, minus one
6682 let max_row = snapshot.max_point().row().as_f32();
6683
6684 // NOTE: The max scroll position for the top of the window
6685 let max_scroll_top = if matches!(snapshot.mode, EditorMode::AutoHeight { .. }) {
6686 (max_row - height_in_lines + 1.).max(0.)
6687 } else {
6688 let settings = EditorSettings::get_global(cx);
6689 match settings.scroll_beyond_last_line {
6690 ScrollBeyondLastLine::OnePage => max_row,
6691 ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.).max(0.),
6692 ScrollBeyondLastLine::VerticalScrollMargin => {
6693 (max_row - height_in_lines + 1. + settings.vertical_scroll_margin)
6694 .max(0.)
6695 }
6696 }
6697 };
6698
6699 // TODO: Autoscrolling for both axes
6700 let mut autoscroll_request = None;
6701 let mut autoscroll_containing_element = false;
6702 let mut autoscroll_horizontally = false;
6703 self.editor.update(cx, |editor, cx| {
6704 autoscroll_request = editor.autoscroll_request();
6705 autoscroll_containing_element =
6706 autoscroll_request.is_some() || editor.has_pending_selection();
6707 // TODO: Is this horizontal or vertical?!
6708 autoscroll_horizontally = editor.autoscroll_vertically(
6709 bounds,
6710 line_height,
6711 max_scroll_top,
6712 window,
6713 cx,
6714 );
6715 snapshot = editor.snapshot(window, cx);
6716 });
6717
6718 let mut scroll_position = snapshot.scroll_position();
6719 // The scroll position is a fractional point, the whole number of which represents
6720 // the top of the window in terms of display rows.
6721 let start_row = DisplayRow(scroll_position.y as u32);
6722 let max_row = snapshot.max_point().row();
6723 let end_row = cmp::min(
6724 (scroll_position.y + height_in_lines).ceil() as u32,
6725 max_row.next_row().0,
6726 );
6727 let end_row = DisplayRow(end_row);
6728
6729 let row_infos = snapshot
6730 .row_infos(start_row)
6731 .take((start_row..end_row).len())
6732 .collect::<Vec<RowInfo>>();
6733 let is_row_soft_wrapped = |row: usize| {
6734 row_infos
6735 .get(row)
6736 .map_or(true, |info| info.buffer_row.is_none())
6737 };
6738
6739 let start_anchor = if start_row == Default::default() {
6740 Anchor::min()
6741 } else {
6742 snapshot.buffer_snapshot.anchor_before(
6743 DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
6744 )
6745 };
6746 let end_anchor = if end_row > max_row {
6747 Anchor::max()
6748 } else {
6749 snapshot.buffer_snapshot.anchor_before(
6750 DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
6751 )
6752 };
6753
6754 let mut highlighted_rows = self
6755 .editor
6756 .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
6757
6758 let is_light = cx.theme().appearance().is_light();
6759
6760 for (ix, row_info) in row_infos.iter().enumerate() {
6761 let Some(diff_status) = row_info.diff_status else {
6762 continue;
6763 };
6764
6765 let background_color = match diff_status.kind {
6766 DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
6767 DiffHunkStatusKind::Deleted => {
6768 cx.theme().colors().version_control_deleted
6769 }
6770 DiffHunkStatusKind::Modified => {
6771 debug_panic!("modified diff status for row info");
6772 continue;
6773 }
6774 };
6775
6776 let unstaged = diff_status.has_secondary_hunk();
6777 let hunk_opacity = if is_light { 0.16 } else { 0.12 };
6778
6779 let staged_highlight = LineHighlight {
6780 background: (background_color.opacity(if is_light {
6781 0.08
6782 } else {
6783 0.06
6784 }))
6785 .into(),
6786 border: Some(if is_light {
6787 background_color.opacity(0.48)
6788 } else {
6789 background_color.opacity(0.36)
6790 }),
6791 };
6792
6793 let unstaged_highlight =
6794 solid_background(background_color.opacity(hunk_opacity)).into();
6795
6796 let background = if unstaged {
6797 unstaged_highlight
6798 } else {
6799 staged_highlight
6800 };
6801
6802 highlighted_rows
6803 .entry(start_row + DisplayRow(ix as u32))
6804 .or_insert(background);
6805 }
6806
6807 let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
6808 start_anchor..end_anchor,
6809 &snapshot.display_snapshot,
6810 cx.theme().colors(),
6811 );
6812 let highlighted_gutter_ranges =
6813 self.editor.read(cx).gutter_highlights_in_range(
6814 start_anchor..end_anchor,
6815 &snapshot.display_snapshot,
6816 cx,
6817 );
6818
6819 let redacted_ranges = self.editor.read(cx).redacted_ranges(
6820 start_anchor..end_anchor,
6821 &snapshot.display_snapshot,
6822 cx,
6823 );
6824
6825 let (local_selections, selected_buffer_ids): (
6826 Vec<Selection<Point>>,
6827 Vec<BufferId>,
6828 ) = self.editor.update(cx, |editor, cx| {
6829 let all_selections = editor.selections.all::<Point>(cx);
6830 let selected_buffer_ids = if editor.is_singleton(cx) {
6831 Vec::new()
6832 } else {
6833 let mut selected_buffer_ids = Vec::with_capacity(all_selections.len());
6834
6835 for selection in all_selections {
6836 for buffer_id in snapshot
6837 .buffer_snapshot
6838 .buffer_ids_for_range(selection.range())
6839 {
6840 if selected_buffer_ids.last() != Some(&buffer_id) {
6841 selected_buffer_ids.push(buffer_id);
6842 }
6843 }
6844 }
6845
6846 selected_buffer_ids
6847 };
6848
6849 let mut selections = editor
6850 .selections
6851 .disjoint_in_range(start_anchor..end_anchor, cx);
6852 selections.extend(editor.selections.pending(cx));
6853
6854 (selections, selected_buffer_ids)
6855 });
6856
6857 let (selections, active_rows, newest_selection_head) = self.layout_selections(
6858 start_anchor,
6859 end_anchor,
6860 &local_selections,
6861 &snapshot,
6862 start_row,
6863 end_row,
6864 window,
6865 cx,
6866 );
6867
6868 let line_numbers = self.layout_line_numbers(
6869 Some(&gutter_hitbox),
6870 gutter_dimensions,
6871 line_height,
6872 scroll_position,
6873 start_row..end_row,
6874 &row_infos,
6875 newest_selection_head,
6876 &snapshot,
6877 window,
6878 cx,
6879 );
6880
6881 let mut crease_toggles =
6882 window.with_element_namespace("crease_toggles", |window| {
6883 self.layout_crease_toggles(
6884 start_row..end_row,
6885 &row_infos,
6886 &active_rows,
6887 &snapshot,
6888 window,
6889 cx,
6890 )
6891 });
6892 let crease_trailers =
6893 window.with_element_namespace("crease_trailers", |window| {
6894 self.layout_crease_trailers(
6895 row_infos.iter().copied(),
6896 &snapshot,
6897 window,
6898 cx,
6899 )
6900 });
6901
6902 let display_hunks = self.layout_gutter_diff_hunks(
6903 line_height,
6904 &gutter_hitbox,
6905 start_row..end_row,
6906 &snapshot,
6907 window,
6908 cx,
6909 );
6910
6911 let mut line_layouts = Self::layout_lines(
6912 start_row..end_row,
6913 &snapshot,
6914 &self.style,
6915 editor_width,
6916 is_row_soft_wrapped,
6917 window,
6918 cx,
6919 );
6920
6921 let longest_line_blame_width = self
6922 .editor
6923 .update(cx, |editor, cx| {
6924 if !editor.show_git_blame_inline {
6925 return None;
6926 }
6927 let blame = editor.blame.as_ref()?;
6928 let blame_entry = blame
6929 .update(cx, |blame, cx| {
6930 let row_infos =
6931 snapshot.row_infos(snapshot.longest_row()).next()?;
6932 blame.blame_for_rows(&[row_infos], cx).next()
6933 })
6934 .flatten()?;
6935 let mut element = render_inline_blame_entry(
6936 self.editor.clone(),
6937 blame,
6938 blame_entry,
6939 &style,
6940 cx,
6941 );
6942 let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance;
6943 Some(
6944 element
6945 .layout_as_root(AvailableSpace::min_size(), window, cx)
6946 .width
6947 + inline_blame_padding,
6948 )
6949 })
6950 .unwrap_or(Pixels::ZERO);
6951
6952 let longest_line_width = layout_line(
6953 snapshot.longest_row(),
6954 &snapshot,
6955 &style,
6956 editor_width,
6957 is_row_soft_wrapped,
6958 window,
6959 cx,
6960 )
6961 .width;
6962
6963 let scrollbar_range_data = ScrollbarRangeData::new(
6964 scrollbar_bounds,
6965 letter_size,
6966 &snapshot,
6967 longest_line_width,
6968 longest_line_blame_width,
6969 &style,
6970 editor_width,
6971 cx,
6972 );
6973
6974 let scroll_range_bounds = scrollbar_range_data.scroll_range;
6975 let mut scroll_width = scroll_range_bounds.size.width;
6976
6977 let sticky_header_excerpt = if snapshot.buffer_snapshot.show_headers() {
6978 snapshot.sticky_header_excerpt(start_row)
6979 } else {
6980 None
6981 };
6982 let sticky_header_excerpt_id =
6983 sticky_header_excerpt.as_ref().map(|top| top.excerpt.id);
6984
6985 let blocks = window.with_element_namespace("blocks", |window| {
6986 self.render_blocks(
6987 start_row..end_row,
6988 &snapshot,
6989 &hitbox,
6990 &text_hitbox,
6991 editor_width,
6992 &mut scroll_width,
6993 &gutter_dimensions,
6994 em_width,
6995 gutter_dimensions.full_width(),
6996 line_height,
6997 &line_layouts,
6998 &local_selections,
6999 &selected_buffer_ids,
7000 is_row_soft_wrapped,
7001 sticky_header_excerpt_id,
7002 window,
7003 cx,
7004 )
7005 });
7006 let mut blocks = match blocks {
7007 Ok(blocks) => blocks,
7008 Err(resized_blocks) => {
7009 self.editor.update(cx, |editor, cx| {
7010 editor.resize_blocks(resized_blocks, autoscroll_request, cx)
7011 });
7012 return self.prepaint(None, bounds, &mut (), window, cx);
7013 }
7014 };
7015
7016 let sticky_buffer_header = sticky_header_excerpt.map(|sticky_header_excerpt| {
7017 window.with_element_namespace("blocks", |window| {
7018 self.layout_sticky_buffer_header(
7019 sticky_header_excerpt,
7020 scroll_position.y,
7021 line_height,
7022 &snapshot,
7023 &hitbox,
7024 &selected_buffer_ids,
7025 window,
7026 cx,
7027 )
7028 })
7029 });
7030
7031 let start_buffer_row =
7032 MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
7033 let end_buffer_row =
7034 MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
7035
7036 let scroll_max = point(
7037 ((scroll_width - scrollbar_bounds.size.width) / em_width).max(0.0),
7038 max_row.as_f32(),
7039 );
7040
7041 self.editor.update(cx, |editor, cx| {
7042 let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
7043
7044 let autoscrolled = if autoscroll_horizontally {
7045 editor.autoscroll_horizontally(
7046 start_row,
7047 editor_width - (letter_size.width / 2.0) + style.scrollbar_width,
7048 scroll_width,
7049 em_width,
7050 &line_layouts,
7051 cx,
7052 )
7053 } else {
7054 false
7055 };
7056
7057 if clamped || autoscrolled {
7058 snapshot = editor.snapshot(window, cx);
7059 scroll_position = snapshot.scroll_position();
7060 }
7061 });
7062
7063 let scroll_pixel_position = point(
7064 scroll_position.x * em_width,
7065 scroll_position.y * line_height,
7066 );
7067
7068 let indent_guides = self.layout_indent_guides(
7069 content_origin,
7070 text_hitbox.origin,
7071 start_buffer_row..end_buffer_row,
7072 scroll_pixel_position,
7073 line_height,
7074 &snapshot,
7075 window,
7076 cx,
7077 );
7078
7079 let crease_trailers =
7080 window.with_element_namespace("crease_trailers", |window| {
7081 self.prepaint_crease_trailers(
7082 crease_trailers,
7083 &line_layouts,
7084 line_height,
7085 content_origin,
7086 scroll_pixel_position,
7087 em_width,
7088 window,
7089 cx,
7090 )
7091 });
7092
7093 let (inline_completion_popover, inline_completion_popover_origin) = self
7094 .editor
7095 .update(cx, |editor, cx| {
7096 editor.render_edit_prediction_popover(
7097 &text_hitbox.bounds,
7098 content_origin,
7099 &snapshot,
7100 start_row..end_row,
7101 scroll_position.y,
7102 scroll_position.y + height_in_lines,
7103 &line_layouts,
7104 line_height,
7105 scroll_pixel_position,
7106 newest_selection_head,
7107 editor_width,
7108 &style,
7109 window,
7110 cx,
7111 )
7112 })
7113 .unzip();
7114
7115 let mut inline_diagnostics = self.layout_inline_diagnostics(
7116 &line_layouts,
7117 &crease_trailers,
7118 content_origin,
7119 scroll_pixel_position,
7120 inline_completion_popover_origin,
7121 start_row,
7122 end_row,
7123 line_height,
7124 em_width,
7125 &style,
7126 window,
7127 cx,
7128 );
7129
7130 let mut inline_blame = None;
7131 if let Some(newest_selection_head) = newest_selection_head {
7132 let display_row = newest_selection_head.row();
7133 if (start_row..end_row).contains(&display_row) {
7134 let line_ix = display_row.minus(start_row) as usize;
7135 let row_info = &row_infos[line_ix];
7136 let line_layout = &line_layouts[line_ix];
7137 let crease_trailer_layout = crease_trailers[line_ix].as_ref();
7138 inline_blame = self.layout_inline_blame(
7139 display_row,
7140 row_info,
7141 line_layout,
7142 crease_trailer_layout,
7143 em_width,
7144 content_origin,
7145 scroll_pixel_position,
7146 line_height,
7147 window,
7148 cx,
7149 );
7150 if inline_blame.is_some() {
7151 // Blame overrides inline diagnostics
7152 inline_diagnostics.remove(&display_row);
7153 }
7154 }
7155 }
7156
7157 let blamed_display_rows = self.layout_blame_entries(
7158 &row_infos,
7159 em_width,
7160 scroll_position,
7161 line_height,
7162 &gutter_hitbox,
7163 gutter_dimensions.git_blame_entries_width,
7164 window,
7165 cx,
7166 );
7167
7168 let scroll_max = point(
7169 ((scroll_width - scrollbar_bounds.size.width) / em_width).max(0.0),
7170 max_scroll_top,
7171 );
7172
7173 self.editor.update(cx, |editor, cx| {
7174 let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
7175
7176 let autoscrolled = if autoscroll_horizontally {
7177 editor.autoscroll_horizontally(
7178 start_row,
7179 editor_width - (letter_size.width / 2.0) + style.scrollbar_width,
7180 scroll_width,
7181 em_width,
7182 &line_layouts,
7183 cx,
7184 )
7185 } else {
7186 false
7187 };
7188
7189 if clamped || autoscrolled {
7190 snapshot = editor.snapshot(window, cx);
7191 scroll_position = snapshot.scroll_position();
7192 }
7193 });
7194
7195 let line_elements = self.prepaint_lines(
7196 start_row,
7197 &mut line_layouts,
7198 line_height,
7199 scroll_pixel_position,
7200 content_origin,
7201 window,
7202 cx,
7203 );
7204
7205 let mut block_start_rows = HashSet::default();
7206
7207 window.with_element_namespace("blocks", |window| {
7208 self.layout_blocks(
7209 &mut blocks,
7210 &mut block_start_rows,
7211 &hitbox,
7212 line_height,
7213 scroll_pixel_position,
7214 window,
7215 cx,
7216 );
7217 });
7218
7219 let cursors = self.collect_cursors(&snapshot, cx);
7220 let visible_row_range = start_row..end_row;
7221 let non_visible_cursors = cursors
7222 .iter()
7223 .any(|c| !visible_row_range.contains(&c.0.row()));
7224
7225 let visible_cursors = self.layout_visible_cursors(
7226 &snapshot,
7227 &selections,
7228 &block_start_rows,
7229 start_row..end_row,
7230 &line_layouts,
7231 &text_hitbox,
7232 content_origin,
7233 scroll_position,
7234 scroll_pixel_position,
7235 line_height,
7236 em_width,
7237 em_advance,
7238 autoscroll_containing_element,
7239 window,
7240 cx,
7241 );
7242
7243 let scrollbars_layout = self.layout_scrollbars(
7244 &snapshot,
7245 scrollbar_range_data,
7246 scroll_position,
7247 non_visible_cursors,
7248 window,
7249 cx,
7250 );
7251
7252 let gutter_settings = EditorSettings::get_global(cx).gutter;
7253
7254 let mut code_actions_indicator = None;
7255 if let Some(newest_selection_head) = newest_selection_head {
7256 let newest_selection_point =
7257 newest_selection_head.to_point(&snapshot.display_snapshot);
7258
7259 if (start_row..end_row).contains(&newest_selection_head.row()) {
7260 self.layout_cursor_popovers(
7261 line_height,
7262 &text_hitbox,
7263 content_origin,
7264 start_row,
7265 scroll_pixel_position,
7266 &line_layouts,
7267 newest_selection_head,
7268 newest_selection_point,
7269 &style,
7270 window,
7271 cx,
7272 );
7273
7274 let show_code_actions = snapshot
7275 .show_code_actions
7276 .unwrap_or(gutter_settings.code_actions);
7277 if show_code_actions {
7278 let newest_selection_point =
7279 newest_selection_head.to_point(&snapshot.display_snapshot);
7280 if !snapshot
7281 .is_line_folded(MultiBufferRow(newest_selection_point.row))
7282 {
7283 let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
7284 MultiBufferRow(newest_selection_point.row),
7285 );
7286 if let Some((buffer, range)) = buffer {
7287 let buffer_id = buffer.remote_id();
7288 let row = range.start.row;
7289 let has_test_indicator = self
7290 .editor
7291 .read(cx)
7292 .tasks
7293 .contains_key(&(buffer_id, row));
7294
7295 if !has_test_indicator {
7296 code_actions_indicator = self
7297 .layout_code_actions_indicator(
7298 line_height,
7299 newest_selection_head,
7300 scroll_pixel_position,
7301 &gutter_dimensions,
7302 &gutter_hitbox,
7303 &display_hunks,
7304 window,
7305 cx,
7306 );
7307 }
7308 }
7309 }
7310 }
7311 }
7312 }
7313
7314 self.layout_gutter_menu(
7315 line_height,
7316 &text_hitbox,
7317 content_origin,
7318 scroll_pixel_position,
7319 gutter_dimensions.width - gutter_dimensions.left_padding,
7320 window,
7321 cx,
7322 );
7323
7324 let test_indicators = if gutter_settings.runnables {
7325 self.layout_run_indicators(
7326 line_height,
7327 start_row..end_row,
7328 scroll_pixel_position,
7329 &gutter_dimensions,
7330 &gutter_hitbox,
7331 &display_hunks,
7332 &snapshot,
7333 window,
7334 cx,
7335 )
7336 } else {
7337 Vec::new()
7338 };
7339
7340 self.layout_signature_help(
7341 &hitbox,
7342 content_origin,
7343 scroll_pixel_position,
7344 newest_selection_head,
7345 start_row,
7346 &line_layouts,
7347 line_height,
7348 em_width,
7349 window,
7350 cx,
7351 );
7352
7353 if !cx.has_active_drag() {
7354 self.layout_hover_popovers(
7355 &snapshot,
7356 &hitbox,
7357 &text_hitbox,
7358 start_row..end_row,
7359 content_origin,
7360 scroll_pixel_position,
7361 &line_layouts,
7362 line_height,
7363 em_width,
7364 window,
7365 cx,
7366 );
7367 }
7368
7369 let mouse_context_menu = self.layout_mouse_context_menu(
7370 &snapshot,
7371 start_row..end_row,
7372 content_origin,
7373 window,
7374 cx,
7375 );
7376
7377 window.with_element_namespace("crease_toggles", |window| {
7378 self.prepaint_crease_toggles(
7379 &mut crease_toggles,
7380 line_height,
7381 &gutter_dimensions,
7382 gutter_settings,
7383 scroll_pixel_position,
7384 &gutter_hitbox,
7385 window,
7386 cx,
7387 )
7388 });
7389
7390 let invisible_symbol_font_size = font_size / 2.;
7391 let tab_invisible = window
7392 .text_system()
7393 .shape_line(
7394 "→".into(),
7395 invisible_symbol_font_size,
7396 &[TextRun {
7397 len: "→".len(),
7398 font: self.style.text.font(),
7399 color: cx.theme().colors().editor_invisible,
7400 background_color: None,
7401 underline: None,
7402 strikethrough: None,
7403 }],
7404 )
7405 .unwrap();
7406 let space_invisible = window
7407 .text_system()
7408 .shape_line(
7409 "•".into(),
7410 invisible_symbol_font_size,
7411 &[TextRun {
7412 len: "•".len(),
7413 font: self.style.text.font(),
7414 color: cx.theme().colors().editor_invisible,
7415 background_color: None,
7416 underline: None,
7417 strikethrough: None,
7418 }],
7419 )
7420 .unwrap();
7421
7422 let mode = snapshot.mode;
7423
7424 let position_map = Rc::new(PositionMap {
7425 size: bounds.size,
7426 visible_row_range,
7427 scroll_pixel_position,
7428 scroll_max,
7429 line_layouts,
7430 line_height,
7431 em_width,
7432 em_advance,
7433 snapshot,
7434 gutter_hitbox: gutter_hitbox.clone(),
7435 text_hitbox: text_hitbox.clone(),
7436 });
7437
7438 self.editor.update(cx, |editor, _| {
7439 editor.last_position_map = Some(position_map.clone())
7440 });
7441
7442 let diff_hunk_controls = self.layout_diff_hunk_controls(
7443 start_row..end_row,
7444 &row_infos,
7445 &text_hitbox,
7446 &position_map,
7447 newest_selection_head,
7448 line_height,
7449 scroll_pixel_position,
7450 &display_hunks,
7451 self.editor.clone(),
7452 window,
7453 cx,
7454 );
7455
7456 EditorLayout {
7457 mode,
7458 position_map,
7459 visible_display_row_range: start_row..end_row,
7460 wrap_guides,
7461 indent_guides,
7462 hitbox,
7463 gutter_hitbox,
7464 display_hunks,
7465 content_origin,
7466 scrollbars_layout,
7467 active_rows,
7468 highlighted_rows,
7469 highlighted_ranges,
7470 highlighted_gutter_ranges,
7471 redacted_ranges,
7472 line_elements,
7473 line_numbers,
7474 blamed_display_rows,
7475 inline_diagnostics,
7476 inline_blame,
7477 blocks,
7478 cursors,
7479 visible_cursors,
7480 selections,
7481 inline_completion_popover,
7482 diff_hunk_controls,
7483 mouse_context_menu,
7484 test_indicators,
7485 code_actions_indicator,
7486 crease_toggles,
7487 crease_trailers,
7488 tab_invisible,
7489 space_invisible,
7490 sticky_buffer_header,
7491 }
7492 })
7493 })
7494 })
7495 }
7496
7497 fn paint(
7498 &mut self,
7499 _: Option<&GlobalElementId>,
7500 bounds: Bounds<gpui::Pixels>,
7501 _: &mut Self::RequestLayoutState,
7502 layout: &mut Self::PrepaintState,
7503 window: &mut Window,
7504 cx: &mut App,
7505 ) {
7506 let focus_handle = self.editor.focus_handle(cx);
7507 let key_context = self
7508 .editor
7509 .update(cx, |editor, cx| editor.key_context(window, cx));
7510
7511 window.set_key_context(key_context);
7512 window.handle_input(
7513 &focus_handle,
7514 ElementInputHandler::new(bounds, self.editor.clone()),
7515 cx,
7516 );
7517 self.register_actions(window, cx);
7518 self.register_key_listeners(window, cx, layout);
7519
7520 let text_style = TextStyleRefinement {
7521 font_size: Some(self.style.text.font_size),
7522 line_height: Some(self.style.text.line_height),
7523 ..Default::default()
7524 };
7525 let rem_size = self.rem_size(cx);
7526 window.with_rem_size(rem_size, |window| {
7527 window.with_text_style(Some(text_style), |window| {
7528 window.with_content_mask(Some(ContentMask { bounds }), |window| {
7529 self.paint_mouse_listeners(layout, window, cx);
7530 self.paint_background(layout, window, cx);
7531 self.paint_indent_guides(layout, window, cx);
7532
7533 if layout.gutter_hitbox.size.width > Pixels::ZERO {
7534 self.paint_blamed_display_rows(layout, window, cx);
7535 self.paint_line_numbers(layout, window, cx);
7536 }
7537
7538 self.paint_text(layout, window, cx);
7539
7540 if layout.gutter_hitbox.size.width > Pixels::ZERO {
7541 self.paint_gutter_highlights(layout, window, cx);
7542 self.paint_gutter_indicators(layout, window, cx);
7543 }
7544
7545 if !layout.blocks.is_empty() {
7546 window.with_element_namespace("blocks", |window| {
7547 self.paint_blocks(layout, window, cx);
7548 });
7549 }
7550
7551 window.with_element_namespace("blocks", |window| {
7552 if let Some(mut sticky_header) = layout.sticky_buffer_header.take() {
7553 sticky_header.paint(window, cx)
7554 }
7555 });
7556
7557 self.paint_scrollbars(layout, window, cx);
7558 self.paint_inline_completion_popover(layout, window, cx);
7559 self.paint_mouse_context_menu(layout, window, cx);
7560 });
7561 })
7562 })
7563 }
7564}
7565
7566pub(super) fn gutter_bounds(
7567 editor_bounds: Bounds<Pixels>,
7568 gutter_dimensions: GutterDimensions,
7569) -> Bounds<Pixels> {
7570 Bounds {
7571 origin: editor_bounds.origin,
7572 size: size(gutter_dimensions.width, editor_bounds.size.height),
7573 }
7574}
7575
7576struct ScrollbarRangeData {
7577 scrollbar_bounds: Bounds<Pixels>,
7578 scroll_range: Bounds<Pixels>,
7579 letter_size: Size<Pixels>,
7580}
7581
7582impl ScrollbarRangeData {
7583 #[allow(clippy::too_many_arguments)]
7584 pub fn new(
7585 scrollbar_bounds: Bounds<Pixels>,
7586 letter_size: Size<Pixels>,
7587 snapshot: &EditorSnapshot,
7588 longest_line_width: Pixels,
7589 longest_line_blame_width: Pixels,
7590 style: &EditorStyle,
7591 editor_width: Pixels,
7592 cx: &mut App,
7593 ) -> ScrollbarRangeData {
7594 // TODO: Simplify this function down, it requires a lot of parameters
7595 let max_row = snapshot.max_point().row();
7596 let text_bounds_size = size(longest_line_width, max_row.0 as f32 * letter_size.height);
7597
7598 let settings = EditorSettings::get_global(cx);
7599 let scroll_beyond_last_line: Pixels = match settings.scroll_beyond_last_line {
7600 ScrollBeyondLastLine::OnePage => px(scrollbar_bounds.size.height / letter_size.height),
7601 ScrollBeyondLastLine::Off => px(1.),
7602 ScrollBeyondLastLine::VerticalScrollMargin => px(1.0 + settings.vertical_scroll_margin),
7603 };
7604
7605 let right_margin = if longest_line_width + longest_line_blame_width >= editor_width {
7606 letter_size.width + style.scrollbar_width
7607 } else {
7608 px(0.0)
7609 };
7610
7611 let overscroll = size(
7612 right_margin + longest_line_blame_width,
7613 letter_size.height * scroll_beyond_last_line,
7614 );
7615
7616 let scroll_range = Bounds {
7617 origin: scrollbar_bounds.origin,
7618 size: text_bounds_size + overscroll,
7619 };
7620
7621 ScrollbarRangeData {
7622 scrollbar_bounds,
7623 scroll_range,
7624 letter_size,
7625 }
7626 }
7627}
7628
7629impl IntoElement for EditorElement {
7630 type Element = Self;
7631
7632 fn into_element(self) -> Self::Element {
7633 self
7634 }
7635}
7636
7637pub struct EditorLayout {
7638 position_map: Rc<PositionMap>,
7639 hitbox: Hitbox,
7640 gutter_hitbox: Hitbox,
7641 content_origin: gpui::Point<Pixels>,
7642 scrollbars_layout: AxisPair<Option<ScrollbarLayout>>,
7643 mode: EditorMode,
7644 wrap_guides: SmallVec<[(Pixels, bool); 2]>,
7645 indent_guides: Option<Vec<IndentGuideLayout>>,
7646 visible_display_row_range: Range<DisplayRow>,
7647 active_rows: BTreeMap<DisplayRow, bool>,
7648 highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
7649 line_elements: SmallVec<[AnyElement; 1]>,
7650 line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
7651 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
7652 blamed_display_rows: Option<Vec<AnyElement>>,
7653 inline_diagnostics: HashMap<DisplayRow, AnyElement>,
7654 inline_blame: Option<AnyElement>,
7655 blocks: Vec<BlockLayout>,
7656 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
7657 highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
7658 redacted_ranges: Vec<Range<DisplayPoint>>,
7659 cursors: Vec<(DisplayPoint, Hsla)>,
7660 visible_cursors: Vec<CursorLayout>,
7661 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
7662 code_actions_indicator: Option<AnyElement>,
7663 test_indicators: Vec<AnyElement>,
7664 crease_toggles: Vec<Option<AnyElement>>,
7665 diff_hunk_controls: Vec<AnyElement>,
7666 crease_trailers: Vec<Option<CreaseTrailerLayout>>,
7667 inline_completion_popover: Option<AnyElement>,
7668 mouse_context_menu: Option<AnyElement>,
7669 tab_invisible: ShapedLine,
7670 space_invisible: ShapedLine,
7671 sticky_buffer_header: Option<AnyElement>,
7672}
7673
7674impl EditorLayout {
7675 fn line_end_overshoot(&self) -> Pixels {
7676 0.15 * self.position_map.line_height
7677 }
7678}
7679
7680struct LineNumberLayout {
7681 shaped_line: ShapedLine,
7682 hitbox: Option<Hitbox>,
7683 display_row: DisplayRow,
7684}
7685
7686struct ColoredRange<T> {
7687 start: T,
7688 end: T,
7689 color: Hsla,
7690}
7691
7692#[derive(Clone)]
7693struct ScrollbarLayout {
7694 hitbox: Hitbox,
7695 visible_range: Range<f32>,
7696 visible: bool,
7697 text_unit_size: Pixels,
7698 thumb_size: Pixels,
7699 axis: Axis,
7700}
7701
7702impl ScrollbarLayout {
7703 const BORDER_WIDTH: Pixels = px(1.0);
7704 const LINE_MARKER_HEIGHT: Pixels = px(2.0);
7705 const MIN_MARKER_HEIGHT: Pixels = px(5.0);
7706 // const MIN_THUMB_HEIGHT: Pixels = px(20.0);
7707
7708 fn thumb_bounds(&self) -> Bounds<Pixels> {
7709 match self.axis {
7710 Axis::Vertical => {
7711 let thumb_top = self.y_for_row(self.visible_range.start);
7712 let thumb_bottom = thumb_top + self.thumb_size;
7713 Bounds::from_corners(
7714 point(self.hitbox.left(), thumb_top),
7715 point(self.hitbox.right(), thumb_bottom),
7716 )
7717 }
7718 Axis::Horizontal => {
7719 let thumb_left =
7720 self.hitbox.left() + self.visible_range.start * self.text_unit_size;
7721 let thumb_right = thumb_left + self.thumb_size;
7722 Bounds::from_corners(
7723 point(thumb_left, self.hitbox.top()),
7724 point(thumb_right, self.hitbox.bottom()),
7725 )
7726 }
7727 }
7728 }
7729
7730 fn y_for_row(&self, row: f32) -> Pixels {
7731 self.hitbox.top() + row * self.text_unit_size
7732 }
7733
7734 fn marker_quads_for_ranges(
7735 &self,
7736 row_ranges: impl IntoIterator<Item = ColoredRange<DisplayRow>>,
7737 column: Option<usize>,
7738 ) -> Vec<PaintQuad> {
7739 struct MinMax {
7740 min: Pixels,
7741 max: Pixels,
7742 }
7743 let (x_range, height_limit) = if let Some(column) = column {
7744 let column_width = px(((self.hitbox.size.width - Self::BORDER_WIDTH).0 / 3.0).floor());
7745 let start = Self::BORDER_WIDTH + (column as f32 * column_width);
7746 let end = start + column_width;
7747 (
7748 Range { start, end },
7749 MinMax {
7750 min: Self::MIN_MARKER_HEIGHT,
7751 max: px(f32::MAX),
7752 },
7753 )
7754 } else {
7755 (
7756 Range {
7757 start: Self::BORDER_WIDTH,
7758 end: self.hitbox.size.width,
7759 },
7760 MinMax {
7761 min: Self::LINE_MARKER_HEIGHT,
7762 max: Self::LINE_MARKER_HEIGHT,
7763 },
7764 )
7765 };
7766
7767 let row_to_y = |row: DisplayRow| row.as_f32() * self.text_unit_size;
7768 let mut pixel_ranges = row_ranges
7769 .into_iter()
7770 .map(|range| {
7771 let start_y = row_to_y(range.start);
7772 let end_y = row_to_y(range.end)
7773 + self
7774 .text_unit_size
7775 .max(height_limit.min)
7776 .min(height_limit.max);
7777 ColoredRange {
7778 start: start_y,
7779 end: end_y,
7780 color: range.color,
7781 }
7782 })
7783 .peekable();
7784
7785 let mut quads = Vec::new();
7786 while let Some(mut pixel_range) = pixel_ranges.next() {
7787 while let Some(next_pixel_range) = pixel_ranges.peek() {
7788 if pixel_range.end >= next_pixel_range.start - px(1.0)
7789 && pixel_range.color == next_pixel_range.color
7790 {
7791 pixel_range.end = next_pixel_range.end.max(pixel_range.end);
7792 pixel_ranges.next();
7793 } else {
7794 break;
7795 }
7796 }
7797
7798 let bounds = Bounds::from_corners(
7799 point(x_range.start, pixel_range.start),
7800 point(x_range.end, pixel_range.end),
7801 );
7802 quads.push(quad(
7803 bounds,
7804 Corners::default(),
7805 pixel_range.color,
7806 Edges::default(),
7807 Hsla::transparent_black(),
7808 ));
7809 }
7810
7811 quads
7812 }
7813}
7814
7815struct CreaseTrailerLayout {
7816 element: AnyElement,
7817 bounds: Bounds<Pixels>,
7818}
7819
7820pub(crate) struct PositionMap {
7821 pub size: Size<Pixels>,
7822 pub line_height: Pixels,
7823 pub scroll_pixel_position: gpui::Point<Pixels>,
7824 pub scroll_max: gpui::Point<f32>,
7825 pub em_width: Pixels,
7826 pub em_advance: Pixels,
7827 pub visible_row_range: Range<DisplayRow>,
7828 pub line_layouts: Vec<LineWithInvisibles>,
7829 pub snapshot: EditorSnapshot,
7830 pub text_hitbox: Hitbox,
7831 pub gutter_hitbox: Hitbox,
7832}
7833
7834#[derive(Debug, Copy, Clone)]
7835pub struct PointForPosition {
7836 pub previous_valid: DisplayPoint,
7837 pub next_valid: DisplayPoint,
7838 pub exact_unclipped: DisplayPoint,
7839 pub column_overshoot_after_line_end: u32,
7840}
7841
7842impl PointForPosition {
7843 pub fn as_valid(&self) -> Option<DisplayPoint> {
7844 if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
7845 Some(self.previous_valid)
7846 } else {
7847 None
7848 }
7849 }
7850}
7851
7852impl PositionMap {
7853 pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
7854 let text_bounds = self.text_hitbox.bounds;
7855 let scroll_position = self.snapshot.scroll_position();
7856 let position = position - text_bounds.origin;
7857 let y = position.y.max(px(0.)).min(self.size.height);
7858 let x = position.x + (scroll_position.x * self.em_width);
7859 let row = ((y / self.line_height) + scroll_position.y) as u32;
7860
7861 let (column, x_overshoot_after_line_end) = if let Some(line) = self
7862 .line_layouts
7863 .get(row as usize - scroll_position.y as usize)
7864 {
7865 if let Some(ix) = line.index_for_x(x) {
7866 (ix as u32, px(0.))
7867 } else {
7868 (line.len as u32, px(0.).max(x - line.width))
7869 }
7870 } else {
7871 (0, x)
7872 };
7873
7874 let mut exact_unclipped = DisplayPoint::new(DisplayRow(row), column);
7875 let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
7876 let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
7877
7878 let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
7879 *exact_unclipped.column_mut() += column_overshoot_after_line_end;
7880 PointForPosition {
7881 previous_valid,
7882 next_valid,
7883 exact_unclipped,
7884 column_overshoot_after_line_end,
7885 }
7886 }
7887}
7888
7889struct BlockLayout {
7890 id: BlockId,
7891 row: Option<DisplayRow>,
7892 element: AnyElement,
7893 available_space: Size<AvailableSpace>,
7894 style: BlockStyle,
7895}
7896
7897pub fn layout_line(
7898 row: DisplayRow,
7899 snapshot: &EditorSnapshot,
7900 style: &EditorStyle,
7901 text_width: Pixels,
7902 is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
7903 window: &mut Window,
7904 cx: &mut App,
7905) -> LineWithInvisibles {
7906 let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
7907 LineWithInvisibles::from_chunks(
7908 chunks,
7909 &style,
7910 MAX_LINE_LEN,
7911 1,
7912 snapshot.mode,
7913 text_width,
7914 is_row_soft_wrapped,
7915 window,
7916 cx,
7917 )
7918 .pop()
7919 .unwrap()
7920}
7921
7922#[derive(Debug)]
7923pub struct IndentGuideLayout {
7924 origin: gpui::Point<Pixels>,
7925 length: Pixels,
7926 single_indent_width: Pixels,
7927 depth: u32,
7928 active: bool,
7929 settings: IndentGuideSettings,
7930}
7931
7932pub struct CursorLayout {
7933 origin: gpui::Point<Pixels>,
7934 block_width: Pixels,
7935 line_height: Pixels,
7936 color: Hsla,
7937 shape: CursorShape,
7938 block_text: Option<ShapedLine>,
7939 cursor_name: Option<AnyElement>,
7940}
7941
7942#[derive(Debug)]
7943pub struct CursorName {
7944 string: SharedString,
7945 color: Hsla,
7946 is_top_row: bool,
7947}
7948
7949impl CursorLayout {
7950 pub fn new(
7951 origin: gpui::Point<Pixels>,
7952 block_width: Pixels,
7953 line_height: Pixels,
7954 color: Hsla,
7955 shape: CursorShape,
7956 block_text: Option<ShapedLine>,
7957 ) -> CursorLayout {
7958 CursorLayout {
7959 origin,
7960 block_width,
7961 line_height,
7962 color,
7963 shape,
7964 block_text,
7965 cursor_name: None,
7966 }
7967 }
7968
7969 pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
7970 Bounds {
7971 origin: self.origin + origin,
7972 size: size(self.block_width, self.line_height),
7973 }
7974 }
7975
7976 fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
7977 match self.shape {
7978 CursorShape::Bar => Bounds {
7979 origin: self.origin + origin,
7980 size: size(px(2.0), self.line_height),
7981 },
7982 CursorShape::Block | CursorShape::Hollow => Bounds {
7983 origin: self.origin + origin,
7984 size: size(self.block_width, self.line_height),
7985 },
7986 CursorShape::Underline => Bounds {
7987 origin: self.origin
7988 + origin
7989 + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)),
7990 size: size(self.block_width, px(2.0)),
7991 },
7992 }
7993 }
7994
7995 pub fn layout(
7996 &mut self,
7997 origin: gpui::Point<Pixels>,
7998 cursor_name: Option<CursorName>,
7999 window: &mut Window,
8000 cx: &mut App,
8001 ) {
8002 if let Some(cursor_name) = cursor_name {
8003 let bounds = self.bounds(origin);
8004 let text_size = self.line_height / 1.5;
8005
8006 let name_origin = if cursor_name.is_top_row {
8007 point(bounds.right() - px(1.), bounds.top())
8008 } else {
8009 match self.shape {
8010 CursorShape::Bar => point(
8011 bounds.right() - px(2.),
8012 bounds.top() - text_size / 2. - px(1.),
8013 ),
8014 _ => point(
8015 bounds.right() - px(1.),
8016 bounds.top() - text_size / 2. - px(1.),
8017 ),
8018 }
8019 };
8020 let mut name_element = div()
8021 .bg(self.color)
8022 .text_size(text_size)
8023 .px_0p5()
8024 .line_height(text_size + px(2.))
8025 .text_color(cursor_name.color)
8026 .child(cursor_name.string.clone())
8027 .into_any_element();
8028
8029 name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx);
8030
8031 self.cursor_name = Some(name_element);
8032 }
8033 }
8034
8035 pub fn paint(&mut self, origin: gpui::Point<Pixels>, window: &mut Window, cx: &mut App) {
8036 let bounds = self.bounds(origin);
8037
8038 //Draw background or border quad
8039 let cursor = if matches!(self.shape, CursorShape::Hollow) {
8040 outline(bounds, self.color)
8041 } else {
8042 fill(bounds, self.color)
8043 };
8044
8045 if let Some(name) = &mut self.cursor_name {
8046 name.paint(window, cx);
8047 }
8048
8049 window.paint_quad(cursor);
8050
8051 if let Some(block_text) = &self.block_text {
8052 block_text
8053 .paint(self.origin + origin, self.line_height, window, cx)
8054 .log_err();
8055 }
8056 }
8057
8058 pub fn shape(&self) -> CursorShape {
8059 self.shape
8060 }
8061}
8062
8063#[derive(Debug)]
8064pub struct HighlightedRange {
8065 pub start_y: Pixels,
8066 pub line_height: Pixels,
8067 pub lines: Vec<HighlightedRangeLine>,
8068 pub color: Hsla,
8069 pub corner_radius: Pixels,
8070}
8071
8072#[derive(Debug)]
8073pub struct HighlightedRangeLine {
8074 pub start_x: Pixels,
8075 pub end_x: Pixels,
8076}
8077
8078impl HighlightedRange {
8079 pub fn paint(&self, bounds: Bounds<Pixels>, window: &mut Window) {
8080 if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
8081 self.paint_lines(self.start_y, &self.lines[0..1], bounds, window);
8082 self.paint_lines(
8083 self.start_y + self.line_height,
8084 &self.lines[1..],
8085 bounds,
8086 window,
8087 );
8088 } else {
8089 self.paint_lines(self.start_y, &self.lines, bounds, window);
8090 }
8091 }
8092
8093 fn paint_lines(
8094 &self,
8095 start_y: Pixels,
8096 lines: &[HighlightedRangeLine],
8097 _bounds: Bounds<Pixels>,
8098 window: &mut Window,
8099 ) {
8100 if lines.is_empty() {
8101 return;
8102 }
8103
8104 let first_line = lines.first().unwrap();
8105 let last_line = lines.last().unwrap();
8106
8107 let first_top_left = point(first_line.start_x, start_y);
8108 let first_top_right = point(first_line.end_x, start_y);
8109
8110 let curve_height = point(Pixels::ZERO, self.corner_radius);
8111 let curve_width = |start_x: Pixels, end_x: Pixels| {
8112 let max = (end_x - start_x) / 2.;
8113 let width = if max < self.corner_radius {
8114 max
8115 } else {
8116 self.corner_radius
8117 };
8118
8119 point(width, Pixels::ZERO)
8120 };
8121
8122 let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
8123 let mut builder = gpui::PathBuilder::fill();
8124 builder.move_to(first_top_right - top_curve_width);
8125 builder.curve_to(first_top_right + curve_height, first_top_right);
8126
8127 let mut iter = lines.iter().enumerate().peekable();
8128 while let Some((ix, line)) = iter.next() {
8129 let bottom_right = point(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
8130
8131 if let Some((_, next_line)) = iter.peek() {
8132 let next_top_right = point(next_line.end_x, bottom_right.y);
8133
8134 match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() {
8135 Ordering::Equal => {
8136 builder.line_to(bottom_right);
8137 }
8138 Ordering::Less => {
8139 let curve_width = curve_width(next_top_right.x, bottom_right.x);
8140 builder.line_to(bottom_right - curve_height);
8141 if self.corner_radius > Pixels::ZERO {
8142 builder.curve_to(bottom_right - curve_width, bottom_right);
8143 }
8144 builder.line_to(next_top_right + curve_width);
8145 if self.corner_radius > Pixels::ZERO {
8146 builder.curve_to(next_top_right + curve_height, next_top_right);
8147 }
8148 }
8149 Ordering::Greater => {
8150 let curve_width = curve_width(bottom_right.x, next_top_right.x);
8151 builder.line_to(bottom_right - curve_height);
8152 if self.corner_radius > Pixels::ZERO {
8153 builder.curve_to(bottom_right + curve_width, bottom_right);
8154 }
8155 builder.line_to(next_top_right - curve_width);
8156 if self.corner_radius > Pixels::ZERO {
8157 builder.curve_to(next_top_right + curve_height, next_top_right);
8158 }
8159 }
8160 }
8161 } else {
8162 let curve_width = curve_width(line.start_x, line.end_x);
8163 builder.line_to(bottom_right - curve_height);
8164 if self.corner_radius > Pixels::ZERO {
8165 builder.curve_to(bottom_right - curve_width, bottom_right);
8166 }
8167
8168 let bottom_left = point(line.start_x, bottom_right.y);
8169 builder.line_to(bottom_left + curve_width);
8170 if self.corner_radius > Pixels::ZERO {
8171 builder.curve_to(bottom_left - curve_height, bottom_left);
8172 }
8173 }
8174 }
8175
8176 if first_line.start_x > last_line.start_x {
8177 let curve_width = curve_width(last_line.start_x, first_line.start_x);
8178 let second_top_left = point(last_line.start_x, start_y + self.line_height);
8179 builder.line_to(second_top_left + curve_height);
8180 if self.corner_radius > Pixels::ZERO {
8181 builder.curve_to(second_top_left + curve_width, second_top_left);
8182 }
8183 let first_bottom_left = point(first_line.start_x, second_top_left.y);
8184 builder.line_to(first_bottom_left - curve_width);
8185 if self.corner_radius > Pixels::ZERO {
8186 builder.curve_to(first_bottom_left - curve_height, first_bottom_left);
8187 }
8188 }
8189
8190 builder.line_to(first_top_left + curve_height);
8191 if self.corner_radius > Pixels::ZERO {
8192 builder.curve_to(first_top_left + top_curve_width, first_top_left);
8193 }
8194 builder.line_to(first_top_right - top_curve_width);
8195
8196 if let Ok(path) = builder.build() {
8197 window.paint_path(path, self.color);
8198 }
8199 }
8200}
8201
8202enum CursorPopoverType {
8203 CodeContextMenu,
8204 EditPrediction,
8205}
8206
8207pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
8208 (delta.pow(1.5) / 100.0).into()
8209}
8210
8211fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
8212 (delta.pow(1.2) / 300.0).into()
8213}
8214
8215pub fn register_action<T: Action>(
8216 editor: &Entity<Editor>,
8217 window: &mut Window,
8218 listener: impl Fn(&mut Editor, &T, &mut Window, &mut Context<Editor>) + 'static,
8219) {
8220 let editor = editor.clone();
8221 window.on_action(TypeId::of::<T>(), move |action, phase, window, cx| {
8222 let action = action.downcast_ref().unwrap();
8223 if phase == DispatchPhase::Bubble {
8224 editor.update(cx, |editor, cx| {
8225 listener(editor, action, window, cx);
8226 })
8227 }
8228 })
8229}
8230
8231fn compute_auto_height_layout(
8232 editor: &mut Editor,
8233 max_lines: usize,
8234 max_line_number_width: Pixels,
8235 known_dimensions: Size<Option<Pixels>>,
8236 available_width: AvailableSpace,
8237 window: &mut Window,
8238 cx: &mut Context<Editor>,
8239) -> Option<Size<Pixels>> {
8240 let width = known_dimensions.width.or({
8241 if let AvailableSpace::Definite(available_width) = available_width {
8242 Some(available_width)
8243 } else {
8244 None
8245 }
8246 })?;
8247 if let Some(height) = known_dimensions.height {
8248 return Some(size(width, height));
8249 }
8250
8251 let style = editor.style.as_ref().unwrap();
8252 let font_id = window.text_system().resolve_font(&style.text.font());
8253 let font_size = style.text.font_size.to_pixels(window.rem_size());
8254 let line_height = style.text.line_height_in_pixels(window.rem_size());
8255 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
8256
8257 let mut snapshot = editor.snapshot(window, cx);
8258 let gutter_dimensions = snapshot
8259 .gutter_dimensions(font_id, font_size, max_line_number_width, cx)
8260 .unwrap_or_default();
8261
8262 editor.gutter_dimensions = gutter_dimensions;
8263 let text_width = width - gutter_dimensions.width;
8264 let overscroll = size(em_width, px(0.));
8265
8266 let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
8267 if editor.set_wrap_width(Some(editor_width), cx) {
8268 snapshot = editor.snapshot(window, cx);
8269 }
8270
8271 let scroll_height = Pixels::from(snapshot.max_point().row().next_row().0) * line_height;
8272 let height = scroll_height
8273 .max(line_height)
8274 .min(line_height * max_lines as f32);
8275
8276 Some(size(width, height))
8277}
8278
8279#[cfg(test)]
8280mod tests {
8281 use super::*;
8282 use crate::{
8283 display_map::{BlockPlacement, BlockProperties},
8284 editor_tests::{init_test, update_test_language_settings},
8285 Editor, MultiBuffer,
8286 };
8287 use gpui::{TestAppContext, VisualTestContext};
8288 use language::language_settings;
8289 use log::info;
8290 use std::num::NonZeroU32;
8291 use util::test::sample_text;
8292
8293 #[gpui::test]
8294 fn test_shape_line_numbers(cx: &mut TestAppContext) {
8295 init_test(cx, |_| {});
8296 let window = cx.add_window(|window, cx| {
8297 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
8298 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
8299 });
8300
8301 let editor = window.root(cx).unwrap();
8302 let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
8303 let line_height = window
8304 .update(cx, |_, window, _| {
8305 style.text.line_height_in_pixels(window.rem_size())
8306 })
8307 .unwrap();
8308 let element = EditorElement::new(&editor, style);
8309 let snapshot = window
8310 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
8311 .unwrap();
8312
8313 let layouts = cx
8314 .update_window(*window, |_, window, cx| {
8315 element.layout_line_numbers(
8316 None,
8317 GutterDimensions {
8318 left_padding: Pixels::ZERO,
8319 right_padding: Pixels::ZERO,
8320 width: px(30.0),
8321 margin: Pixels::ZERO,
8322 git_blame_entries_width: None,
8323 },
8324 line_height,
8325 gpui::Point::default(),
8326 DisplayRow(0)..DisplayRow(6),
8327 &(0..6)
8328 .map(|row| RowInfo {
8329 buffer_row: Some(row),
8330 ..Default::default()
8331 })
8332 .collect::<Vec<_>>(),
8333 Some(DisplayPoint::new(DisplayRow(0), 0)),
8334 &snapshot,
8335 window,
8336 cx,
8337 )
8338 })
8339 .unwrap();
8340 assert_eq!(layouts.len(), 6);
8341
8342 let relative_rows = window
8343 .update(cx, |editor, window, cx| {
8344 let snapshot = editor.snapshot(window, cx);
8345 element.calculate_relative_line_numbers(
8346 &snapshot,
8347 &(DisplayRow(0)..DisplayRow(6)),
8348 Some(DisplayRow(3)),
8349 )
8350 })
8351 .unwrap();
8352 assert_eq!(relative_rows[&DisplayRow(0)], 3);
8353 assert_eq!(relative_rows[&DisplayRow(1)], 2);
8354 assert_eq!(relative_rows[&DisplayRow(2)], 1);
8355 // current line has no relative number
8356 assert_eq!(relative_rows[&DisplayRow(4)], 1);
8357 assert_eq!(relative_rows[&DisplayRow(5)], 2);
8358
8359 // works if cursor is before screen
8360 let relative_rows = window
8361 .update(cx, |editor, window, cx| {
8362 let snapshot = editor.snapshot(window, cx);
8363 element.calculate_relative_line_numbers(
8364 &snapshot,
8365 &(DisplayRow(3)..DisplayRow(6)),
8366 Some(DisplayRow(1)),
8367 )
8368 })
8369 .unwrap();
8370 assert_eq!(relative_rows.len(), 3);
8371 assert_eq!(relative_rows[&DisplayRow(3)], 2);
8372 assert_eq!(relative_rows[&DisplayRow(4)], 3);
8373 assert_eq!(relative_rows[&DisplayRow(5)], 4);
8374
8375 // works if cursor is after screen
8376 let relative_rows = window
8377 .update(cx, |editor, window, cx| {
8378 let snapshot = editor.snapshot(window, cx);
8379 element.calculate_relative_line_numbers(
8380 &snapshot,
8381 &(DisplayRow(0)..DisplayRow(3)),
8382 Some(DisplayRow(6)),
8383 )
8384 })
8385 .unwrap();
8386 assert_eq!(relative_rows.len(), 3);
8387 assert_eq!(relative_rows[&DisplayRow(0)], 5);
8388 assert_eq!(relative_rows[&DisplayRow(1)], 4);
8389 assert_eq!(relative_rows[&DisplayRow(2)], 3);
8390 }
8391
8392 #[gpui::test]
8393 async fn test_vim_visual_selections(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let window = cx.add_window(|window, cx| {
8397 let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
8398 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
8399 });
8400 let cx = &mut VisualTestContext::from_window(*window, cx);
8401 let editor = window.root(cx).unwrap();
8402 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
8403
8404 window
8405 .update(cx, |editor, window, cx| {
8406 editor.cursor_shape = CursorShape::Block;
8407 editor.change_selections(None, window, cx, |s| {
8408 s.select_ranges([
8409 Point::new(0, 0)..Point::new(1, 0),
8410 Point::new(3, 2)..Point::new(3, 3),
8411 Point::new(5, 6)..Point::new(6, 0),
8412 ]);
8413 });
8414 })
8415 .unwrap();
8416
8417 let (_, state) = cx.draw(
8418 point(px(500.), px(500.)),
8419 size(px(500.), px(500.)),
8420 |_, _| EditorElement::new(&editor, style),
8421 );
8422
8423 assert_eq!(state.selections.len(), 1);
8424 let local_selections = &state.selections[0].1;
8425 assert_eq!(local_selections.len(), 3);
8426 // moves cursor back one line
8427 assert_eq!(
8428 local_selections[0].head,
8429 DisplayPoint::new(DisplayRow(0), 6)
8430 );
8431 assert_eq!(
8432 local_selections[0].range,
8433 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0)
8434 );
8435
8436 // moves cursor back one column
8437 assert_eq!(
8438 local_selections[1].range,
8439 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 3)
8440 );
8441 assert_eq!(
8442 local_selections[1].head,
8443 DisplayPoint::new(DisplayRow(3), 2)
8444 );
8445
8446 // leaves cursor on the max point
8447 assert_eq!(
8448 local_selections[2].range,
8449 DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(6), 0)
8450 );
8451 assert_eq!(
8452 local_selections[2].head,
8453 DisplayPoint::new(DisplayRow(6), 0)
8454 );
8455
8456 // active lines does not include 1 (even though the range of the selection does)
8457 assert_eq!(
8458 state.active_rows.keys().cloned().collect::<Vec<_>>(),
8459 vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
8460 );
8461
8462 // multi-buffer support
8463 // in DisplayPoint coordinates, this is what we're dealing with:
8464 // 0: [[file
8465 // 1: header
8466 // 2: section]]
8467 // 3: aaaaaa
8468 // 4: bbbbbb
8469 // 5: cccccc
8470 // 6:
8471 // 7: [[footer]]
8472 // 8: [[header]]
8473 // 9: ffffff
8474 // 10: gggggg
8475 // 11: hhhhhh
8476 // 12:
8477 // 13: [[footer]]
8478 // 14: [[file
8479 // 15: header
8480 // 16: section]]
8481 // 17: bbbbbb
8482 // 18: cccccc
8483 // 19: dddddd
8484 // 20: [[footer]]
8485 let window = cx.add_window(|window, cx| {
8486 let buffer = MultiBuffer::build_multi(
8487 [
8488 (
8489 &(sample_text(8, 6, 'a') + "\n"),
8490 vec![
8491 Point::new(0, 0)..Point::new(3, 0),
8492 Point::new(4, 0)..Point::new(7, 0),
8493 ],
8494 ),
8495 (
8496 &(sample_text(8, 6, 'a') + "\n"),
8497 vec![Point::new(1, 0)..Point::new(3, 0)],
8498 ),
8499 ],
8500 cx,
8501 );
8502 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
8503 });
8504 let editor = window.root(cx).unwrap();
8505 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
8506 let _state = window.update(cx, |editor, window, cx| {
8507 editor.cursor_shape = CursorShape::Block;
8508 editor.change_selections(None, window, cx, |s| {
8509 s.select_display_ranges([
8510 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0),
8511 DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0),
8512 ]);
8513 });
8514 });
8515
8516 let (_, state) = cx.draw(
8517 point(px(500.), px(500.)),
8518 size(px(500.), px(500.)),
8519 |_, _| EditorElement::new(&editor, style),
8520 );
8521 assert_eq!(state.selections.len(), 1);
8522 let local_selections = &state.selections[0].1;
8523 assert_eq!(local_selections.len(), 2);
8524
8525 // moves cursor on excerpt boundary back a line
8526 // and doesn't allow selection to bleed through
8527 assert_eq!(
8528 local_selections[0].range,
8529 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0)
8530 );
8531 assert_eq!(
8532 local_selections[0].head,
8533 DisplayPoint::new(DisplayRow(6), 0)
8534 );
8535 // moves cursor on buffer boundary back two lines
8536 // and doesn't allow selection to bleed through
8537 assert_eq!(
8538 local_selections[1].range,
8539 DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0)
8540 );
8541 assert_eq!(
8542 local_selections[1].head,
8543 DisplayPoint::new(DisplayRow(12), 0)
8544 );
8545 }
8546
8547 #[gpui::test]
8548 fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
8549 init_test(cx, |_| {});
8550
8551 let window = cx.add_window(|window, cx| {
8552 let buffer = MultiBuffer::build_simple("", cx);
8553 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
8554 });
8555 let cx = &mut VisualTestContext::from_window(*window, cx);
8556 let editor = window.root(cx).unwrap();
8557 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
8558 window
8559 .update(cx, |editor, window, cx| {
8560 editor.set_placeholder_text("hello", cx);
8561 editor.insert_blocks(
8562 [BlockProperties {
8563 style: BlockStyle::Fixed,
8564 placement: BlockPlacement::Above(Anchor::min()),
8565 height: 3,
8566 render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
8567 priority: 0,
8568 }],
8569 None,
8570 cx,
8571 );
8572
8573 // Blur the editor so that it displays placeholder text.
8574 window.blur();
8575 })
8576 .unwrap();
8577
8578 let (_, state) = cx.draw(
8579 point(px(500.), px(500.)),
8580 size(px(500.), px(500.)),
8581 |_, _| EditorElement::new(&editor, style),
8582 );
8583 assert_eq!(state.position_map.line_layouts.len(), 4);
8584 assert_eq!(state.line_numbers.len(), 1);
8585 assert_eq!(
8586 state
8587 .line_numbers
8588 .get(&MultiBufferRow(0))
8589 .map(|line_number| line_number.shaped_line.text.as_ref()),
8590 Some("1")
8591 );
8592 }
8593
8594 #[gpui::test]
8595 fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
8596 const TAB_SIZE: u32 = 4;
8597
8598 let input_text = "\t \t|\t| a b";
8599 let expected_invisibles = vec![
8600 Invisible::Tab {
8601 line_start_offset: 0,
8602 line_end_offset: TAB_SIZE as usize,
8603 },
8604 Invisible::Whitespace {
8605 line_offset: TAB_SIZE as usize,
8606 },
8607 Invisible::Tab {
8608 line_start_offset: TAB_SIZE as usize + 1,
8609 line_end_offset: TAB_SIZE as usize * 2,
8610 },
8611 Invisible::Tab {
8612 line_start_offset: TAB_SIZE as usize * 2 + 1,
8613 line_end_offset: TAB_SIZE as usize * 3,
8614 },
8615 Invisible::Whitespace {
8616 line_offset: TAB_SIZE as usize * 3 + 1,
8617 },
8618 Invisible::Whitespace {
8619 line_offset: TAB_SIZE as usize * 3 + 3,
8620 },
8621 ];
8622 assert_eq!(
8623 expected_invisibles.len(),
8624 input_text
8625 .chars()
8626 .filter(|initial_char| initial_char.is_whitespace())
8627 .count(),
8628 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
8629 );
8630
8631 for show_line_numbers in [true, false] {
8632 init_test(cx, |s| {
8633 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
8634 s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
8635 });
8636
8637 let actual_invisibles = collect_invisibles_from_new_editor(
8638 cx,
8639 EditorMode::Full,
8640 input_text,
8641 px(500.0),
8642 show_line_numbers,
8643 );
8644
8645 assert_eq!(expected_invisibles, actual_invisibles);
8646 }
8647 }
8648
8649 #[gpui::test]
8650 fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
8651 init_test(cx, |s| {
8652 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
8653 s.defaults.tab_size = NonZeroU32::new(4);
8654 });
8655
8656 for editor_mode_without_invisibles in [
8657 EditorMode::SingleLine { auto_width: false },
8658 EditorMode::AutoHeight { max_lines: 100 },
8659 ] {
8660 for show_line_numbers in [true, false] {
8661 let invisibles = collect_invisibles_from_new_editor(
8662 cx,
8663 editor_mode_without_invisibles,
8664 "\t\t\t| | a b",
8665 px(500.0),
8666 show_line_numbers,
8667 );
8668 assert!(invisibles.is_empty(),
8669 "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
8670 }
8671 }
8672 }
8673
8674 #[gpui::test]
8675 fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
8676 let tab_size = 4;
8677 let input_text = "a\tbcd ".repeat(9);
8678 let repeated_invisibles = [
8679 Invisible::Tab {
8680 line_start_offset: 1,
8681 line_end_offset: tab_size as usize,
8682 },
8683 Invisible::Whitespace {
8684 line_offset: tab_size as usize + 3,
8685 },
8686 Invisible::Whitespace {
8687 line_offset: tab_size as usize + 4,
8688 },
8689 Invisible::Whitespace {
8690 line_offset: tab_size as usize + 5,
8691 },
8692 Invisible::Whitespace {
8693 line_offset: tab_size as usize + 6,
8694 },
8695 Invisible::Whitespace {
8696 line_offset: tab_size as usize + 7,
8697 },
8698 ];
8699 let expected_invisibles = std::iter::once(repeated_invisibles)
8700 .cycle()
8701 .take(9)
8702 .flatten()
8703 .collect::<Vec<_>>();
8704 assert_eq!(
8705 expected_invisibles.len(),
8706 input_text
8707 .chars()
8708 .filter(|initial_char| initial_char.is_whitespace())
8709 .count(),
8710 "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
8711 );
8712 info!("Expected invisibles: {expected_invisibles:?}");
8713
8714 init_test(cx, |_| {});
8715
8716 // Put the same string with repeating whitespace pattern into editors of various size,
8717 // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
8718 let resize_step = 10.0;
8719 let mut editor_width = 200.0;
8720 while editor_width <= 1000.0 {
8721 for show_line_numbers in [true, false] {
8722 update_test_language_settings(cx, |s| {
8723 s.defaults.tab_size = NonZeroU32::new(tab_size);
8724 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
8725 s.defaults.preferred_line_length = Some(editor_width as u32);
8726 s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
8727 });
8728
8729 let actual_invisibles = collect_invisibles_from_new_editor(
8730 cx,
8731 EditorMode::Full,
8732 &input_text,
8733 px(editor_width),
8734 show_line_numbers,
8735 );
8736
8737 // Whatever the editor size is, ensure it has the same invisible kinds in the same order
8738 // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
8739 let mut i = 0;
8740 for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
8741 i = actual_index;
8742 match expected_invisibles.get(i) {
8743 Some(expected_invisible) => match (expected_invisible, actual_invisible) {
8744 (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
8745 | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
8746 _ => {
8747 panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
8748 }
8749 },
8750 None => {
8751 panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
8752 }
8753 }
8754 }
8755 let missing_expected_invisibles = &expected_invisibles[i + 1..];
8756 assert!(
8757 missing_expected_invisibles.is_empty(),
8758 "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
8759 );
8760
8761 editor_width += resize_step;
8762 }
8763 }
8764 }
8765
8766 fn collect_invisibles_from_new_editor(
8767 cx: &mut TestAppContext,
8768 editor_mode: EditorMode,
8769 input_text: &str,
8770 editor_width: Pixels,
8771 show_line_numbers: bool,
8772 ) -> Vec<Invisible> {
8773 info!(
8774 "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
8775 editor_width.0
8776 );
8777 let window = cx.add_window(|window, cx| {
8778 let buffer = MultiBuffer::build_simple(input_text, cx);
8779 Editor::new(editor_mode, buffer, None, true, window, cx)
8780 });
8781 let cx = &mut VisualTestContext::from_window(*window, cx);
8782 let editor = window.root(cx).unwrap();
8783
8784 let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
8785 window
8786 .update(cx, |editor, _, cx| {
8787 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
8788 editor.set_wrap_width(Some(editor_width), cx);
8789 editor.set_show_line_numbers(show_line_numbers, cx);
8790 })
8791 .unwrap();
8792 let (_, state) = cx.draw(
8793 point(px(500.), px(500.)),
8794 size(px(500.), px(500.)),
8795 |_, _| EditorElement::new(&editor, style),
8796 );
8797 state
8798 .position_map
8799 .line_layouts
8800 .iter()
8801 .flat_map(|line_with_invisibles| &line_with_invisibles.invisibles)
8802 .cloned()
8803 .collect()
8804 }
8805}
8806
8807fn diff_hunk_controls(
8808 row: u32,
8809 status: &DiffHunkStatus,
8810 hunk_range: Range<Anchor>,
8811 is_created_file: bool,
8812 line_height: Pixels,
8813 editor: &Entity<Editor>,
8814 cx: &mut App,
8815) -> AnyElement {
8816 h_flex()
8817 .h(line_height)
8818 .mr_1()
8819 .gap_1()
8820 .px_0p5()
8821 .pb_1()
8822 .border_x_1()
8823 .border_b_1()
8824 .border_color(cx.theme().colors().border_variant)
8825 .rounded_b_lg()
8826 .bg(cx.theme().colors().editor_background)
8827 .gap_1()
8828 .occlude()
8829 .shadow_md()
8830 .child(if status.has_secondary_hunk() {
8831 Button::new(("stage", row as u64), "Stage")
8832 .alpha(if status.is_pending() { 0.66 } else { 1.0 })
8833 .tooltip({
8834 let focus_handle = editor.focus_handle(cx);
8835 move |window, cx| {
8836 Tooltip::for_action_in(
8837 "Stage Hunk",
8838 &::git::ToggleStaged,
8839 &focus_handle,
8840 window,
8841 cx,
8842 )
8843 }
8844 })
8845 .on_click({
8846 let editor = editor.clone();
8847 move |_event, _window, cx| {
8848 editor.update(cx, |editor, cx| {
8849 editor.stage_or_unstage_diff_hunks(
8850 true,
8851 vec![hunk_range.start..hunk_range.start],
8852 cx,
8853 );
8854 });
8855 }
8856 })
8857 } else {
8858 Button::new(("unstage", row as u64), "Unstage")
8859 .alpha(if status.is_pending() { 0.66 } else { 1.0 })
8860 .tooltip({
8861 let focus_handle = editor.focus_handle(cx);
8862 move |window, cx| {
8863 Tooltip::for_action_in(
8864 "Unstage Hunk",
8865 &::git::ToggleStaged,
8866 &focus_handle,
8867 window,
8868 cx,
8869 )
8870 }
8871 })
8872 .on_click({
8873 let editor = editor.clone();
8874 move |_event, _window, cx| {
8875 editor.update(cx, |editor, cx| {
8876 editor.stage_or_unstage_diff_hunks(
8877 false,
8878 vec![hunk_range.start..hunk_range.start],
8879 cx,
8880 );
8881 });
8882 }
8883 })
8884 })
8885 .child(
8886 Button::new("restore", "Restore")
8887 .tooltip({
8888 let focus_handle = editor.focus_handle(cx);
8889 move |window, cx| {
8890 Tooltip::for_action_in(
8891 "Restore Hunk",
8892 &::git::Restore,
8893 &focus_handle,
8894 window,
8895 cx,
8896 )
8897 }
8898 })
8899 .on_click({
8900 let editor = editor.clone();
8901 move |_event, window, cx| {
8902 editor.update(cx, |editor, cx| {
8903 let snapshot = editor.snapshot(window, cx);
8904 let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
8905 editor.restore_hunks_in_ranges(vec![point..point], window, cx);
8906 });
8907 }
8908 })
8909 .disabled(is_created_file),
8910 )
8911 .when(
8912 !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
8913 |el| {
8914 el.child(
8915 IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
8916 .shape(IconButtonShape::Square)
8917 .icon_size(IconSize::Small)
8918 // .disabled(!has_multiple_hunks)
8919 .tooltip({
8920 let focus_handle = editor.focus_handle(cx);
8921 move |window, cx| {
8922 Tooltip::for_action_in(
8923 "Next Hunk",
8924 &GoToHunk,
8925 &focus_handle,
8926 window,
8927 cx,
8928 )
8929 }
8930 })
8931 .on_click({
8932 let editor = editor.clone();
8933 move |_event, window, cx| {
8934 editor.update(cx, |editor, cx| {
8935 let snapshot = editor.snapshot(window, cx);
8936 let position =
8937 hunk_range.end.to_point(&snapshot.buffer_snapshot);
8938 editor.go_to_hunk_before_or_after_position(
8939 &snapshot,
8940 position,
8941 Direction::Next,
8942 window,
8943 cx,
8944 );
8945 editor.expand_selected_diff_hunks(cx);
8946 });
8947 }
8948 }),
8949 )
8950 .child(
8951 IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
8952 .shape(IconButtonShape::Square)
8953 .icon_size(IconSize::Small)
8954 // .disabled(!has_multiple_hunks)
8955 .tooltip({
8956 let focus_handle = editor.focus_handle(cx);
8957 move |window, cx| {
8958 Tooltip::for_action_in(
8959 "Previous Hunk",
8960 &GoToPreviousHunk,
8961 &focus_handle,
8962 window,
8963 cx,
8964 )
8965 }
8966 })
8967 .on_click({
8968 let editor = editor.clone();
8969 move |_event, window, cx| {
8970 editor.update(cx, |editor, cx| {
8971 let snapshot = editor.snapshot(window, cx);
8972 let point =
8973 hunk_range.start.to_point(&snapshot.buffer_snapshot);
8974 editor.go_to_hunk_before_or_after_position(
8975 &snapshot,
8976 point,
8977 Direction::Prev,
8978 window,
8979 cx,
8980 );
8981 editor.expand_selected_diff_hunks(cx);
8982 });
8983 }
8984 }),
8985 )
8986 },
8987 )
8988 .into_any_element()
8989}