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