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