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