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