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