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