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