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