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