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