Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #55576 This PR adds a new `editor: toggle breadcrumb` actions that toggles the breadcrumbs for the active item without persisting on `settings.json`. Similar to what `editor: toggle minimap` does. https://github.com/user-attachments/assets/7179a0c8-6a5e-4716-86b8-abde8aa61f68 A simple test was added to check that using this action doesn't change the settings, and that changing an unrelated setting keeps the toggled action as the source of truth for the breadcrumb visibility. Release Notes: - Added `editor: toggle breadcrumb` action to toggle breadcrumb of active item without persisting on `settings.json`
12024 lines
476 KiB
Rust
12024 lines
476 KiB
Rust
mod header;
|
||
mod mouse;
|
||
|
||
#[cfg(test)]
|
||
pub(crate) use header::StickyHeader;
|
||
pub(crate) use header::{header_jump_data, render_buffer_header};
|
||
|
||
use crate::{
|
||
BUFFER_HEADER_PADDING, BlockId, ChunkRendererContext, ChunkReplacement, CodeActionSource,
|
||
ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker,
|
||
ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow,
|
||
EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot,
|
||
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
|
||
HandleInput, HoveredCursor, InlayHintRefreshReason, LineDown, LineHighlight, LineUp,
|
||
MAX_LINE_LEN, MINIMAP_FONT_SIZE, PageDown, PageUp, Point, RowExt, RowRangeExt, Selection,
|
||
SelectionDragState, SizingBehavior, SoftWrap, ToPoint,
|
||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||
column_pixels,
|
||
display_map::{
|
||
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
|
||
HighlightKey, HighlightedChunk, ToDisplayPoint,
|
||
},
|
||
editor_settings::{
|
||
CurrentLineHighlight, DocumentColorsRenderMode, Minimap, MinimapThumb, MinimapThumbBorder,
|
||
ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, ShowMinimap,
|
||
},
|
||
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
|
||
hover_popover::{
|
||
self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||
POPOVER_RIGHT_OFFSET,
|
||
},
|
||
inlay_hint_settings,
|
||
scroll::{
|
||
ActiveScrollbarState, ScrollOffset, ScrollPixelOffset, ScrollbarThumbState,
|
||
scroll_amount::ScrollAmount,
|
||
},
|
||
};
|
||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||
use collections::{BTreeMap, HashMap, HashSet};
|
||
use feature_flags::{DiffReviewFeatureFlag, FeatureFlagAppExt as _};
|
||
use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage};
|
||
use gpui::{
|
||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||
Bounds, ClipboardItem, ContentMask, Context, Corners, CursorStyle, DispatchPhase, Edges,
|
||
Element, ElementInputHandler, Entity, Focusable as _, Font, FontId, FontWeight,
|
||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||
ParentElement, Pixels, ScrollHandle, ShapedLine, SharedString, Size,
|
||
StatefulInteractiveElement, Style, Styled, StyledText, TaskExt, TextAlign, TextRun,
|
||
TextStyleRefinement, WeakEntity, Window, div, fill, outline, pattern_slash, point, px, quad,
|
||
relative, size, solid_background, transparent_black,
|
||
};
|
||
use itertools::Itertools;
|
||
use language::{
|
||
HighlightedText, IndentGuideSettings, LanguageAwareStyling,
|
||
language_settings::ShowWhitespaceSetting,
|
||
};
|
||
use markdown::Markdown;
|
||
use multi_buffer::{
|
||
Anchor, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint, MultiBufferRow, RowInfo,
|
||
};
|
||
|
||
use project::{
|
||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||
project_settings::ProjectSettings,
|
||
};
|
||
use settings::{
|
||
GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring,
|
||
Settings,
|
||
};
|
||
use smallvec::{SmallVec, smallvec};
|
||
use std::{
|
||
any::TypeId,
|
||
borrow::Cow,
|
||
cell::Cell,
|
||
cmp::{self, Ordering},
|
||
fmt::{self, Write},
|
||
iter, mem,
|
||
ops::{Deref, Range},
|
||
rc::Rc,
|
||
sync::Arc,
|
||
time::Duration,
|
||
};
|
||
use sum_tree::Bias;
|
||
use text::BufferId;
|
||
use theme::{ActiveTheme, Appearance, PlayerColor};
|
||
use theme_settings::BufferLineHeight;
|
||
use ui::utils::ensure_minimum_contrast;
|
||
use ui::{ButtonLike, POPOVER_Y_PADDING, Tooltip, prelude::*, scrollbars::ShowScrollbar};
|
||
use unicode_segmentation::UnicodeSegmentation;
|
||
use util::{ResultExt, debug_panic};
|
||
use workspace::{
|
||
CollaboratorId, ItemHandle, Workspace,
|
||
item::{Item, ItemBufferKind},
|
||
};
|
||
|
||
/// Determines what kinds of highlights should be applied to a lines background.
|
||
#[derive(Clone, Copy, Default)]
|
||
struct LineHighlightSpec {
|
||
selection: bool,
|
||
breakpoint: bool,
|
||
_active_stack_frame: bool,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
struct SelectionLayout {
|
||
head: DisplayPoint,
|
||
cursor_shape: CursorShape,
|
||
is_newest: bool,
|
||
is_local: bool,
|
||
range: Range<DisplayPoint>,
|
||
active_rows: Range<DisplayRow>,
|
||
user_name: Option<SharedString>,
|
||
}
|
||
|
||
struct InlineBlameLayout {
|
||
element: AnyElement,
|
||
bounds: Bounds<Pixels>,
|
||
buffer_id: BufferId,
|
||
entry: BlameEntry,
|
||
}
|
||
|
||
impl SelectionLayout {
|
||
fn new<T: ToPoint + ToDisplayPoint + Clone>(
|
||
selection: Selection<T>,
|
||
line_mode: bool,
|
||
cursor_offset: bool,
|
||
cursor_shape: CursorShape,
|
||
map: &DisplaySnapshot,
|
||
is_newest: bool,
|
||
is_local: bool,
|
||
user_name: Option<SharedString>,
|
||
) -> Self {
|
||
let point_selection = selection.map(|p| p.to_point(map.buffer_snapshot()));
|
||
let display_selection = point_selection.map(|p| p.to_display_point(map));
|
||
let mut range = display_selection.range();
|
||
let mut head = display_selection.head();
|
||
let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
|
||
..map.next_line_boundary(point_selection.end).1.row();
|
||
|
||
// vim visual line mode
|
||
if line_mode {
|
||
let point_range = map.expand_to_line(point_selection.range());
|
||
range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
|
||
}
|
||
|
||
// any vim visual mode (including line mode)
|
||
if cursor_offset && !range.is_empty() && !selection.reversed {
|
||
if head.column() > 0 {
|
||
head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left);
|
||
} else if head.row().0 > 0 && head != map.max_point() {
|
||
head = map.clip_point(
|
||
DisplayPoint::new(
|
||
head.row().previous_row(),
|
||
map.line_len(head.row().previous_row()),
|
||
),
|
||
Bias::Left,
|
||
);
|
||
// updating range.end is a no-op unless you're cursor is
|
||
// on the newline containing a multi-buffer divider
|
||
// in which case the clip_point may have moved the head up
|
||
// an additional row.
|
||
range.end = DisplayPoint::new(head.row().next_row(), 0);
|
||
active_rows.end = head.row();
|
||
}
|
||
}
|
||
|
||
Self {
|
||
head,
|
||
cursor_shape,
|
||
is_newest,
|
||
is_local,
|
||
range,
|
||
active_rows,
|
||
user_name,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Default)]
|
||
struct RenderBlocksOutput {
|
||
// We store spacer blocks separately because they paint in a different order
|
||
// (spacers -> indent guides -> non-spacers)
|
||
non_spacer_blocks: Vec<BlockLayout>,
|
||
spacer_blocks: Vec<BlockLayout>,
|
||
row_block_types: HashMap<DisplayRow, bool>,
|
||
resized_blocks: Option<HashMap<CustomBlockId, u32>>,
|
||
}
|
||
|
||
pub struct EditorElement {
|
||
editor: Entity<Editor>,
|
||
style: EditorStyle,
|
||
split_side: Option<SplitSide>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum SplitSide {
|
||
Left,
|
||
Right,
|
||
}
|
||
|
||
impl EditorElement {
|
||
pub(crate) const SCROLLBAR_WIDTH: Pixels = ui::EDITOR_SCROLLBAR_WIDTH;
|
||
|
||
pub fn new(editor: &Entity<Editor>, style: EditorStyle) -> Self {
|
||
Self {
|
||
editor: editor.clone(),
|
||
style,
|
||
split_side: None,
|
||
}
|
||
}
|
||
|
||
pub fn set_split_side(&mut self, side: SplitSide) {
|
||
self.split_side = Some(side);
|
||
}
|
||
|
||
fn register_actions(&self, window: &mut Window, cx: &mut App) {
|
||
let editor = &self.editor;
|
||
editor.update(cx, |editor, cx| {
|
||
for action in editor.editor_actions.borrow().values() {
|
||
(action)(editor, window, cx)
|
||
}
|
||
});
|
||
|
||
crate::rust_analyzer_ext::apply_related_actions(editor, window, cx);
|
||
crate::clangd_ext::apply_related_actions(editor, window, cx);
|
||
|
||
register_action(editor, window, Editor::open_context_menu);
|
||
register_action(editor, window, Editor::move_left);
|
||
register_action(editor, window, Editor::move_right);
|
||
register_action(editor, window, Editor::move_down);
|
||
register_action(editor, window, Editor::move_down_by_lines);
|
||
register_action(editor, window, Editor::select_down_by_lines);
|
||
register_action(editor, window, Editor::move_up);
|
||
register_action(editor, window, Editor::move_up_by_lines);
|
||
register_action(editor, window, Editor::select_up_by_lines);
|
||
register_action(editor, window, Editor::select_page_down);
|
||
register_action(editor, window, Editor::select_page_up);
|
||
register_action(editor, window, Editor::cancel);
|
||
register_action(editor, window, Editor::blame_hover);
|
||
register_action(editor, window, Editor::next_snippet_tabstop);
|
||
register_action(editor, window, Editor::previous_snippet_tabstop);
|
||
register_action(editor, window, Editor::copy);
|
||
register_action(editor, window, Editor::copy_and_trim);
|
||
register_action(editor, window, Editor::diff_clipboard_with_selection);
|
||
register_action(editor, window, Editor::move_page_up);
|
||
register_action(editor, window, Editor::move_page_down);
|
||
register_action(editor, window, Editor::next_screen);
|
||
register_action(editor, window, Editor::scroll_cursor_top);
|
||
register_action(editor, window, Editor::scroll_cursor_center);
|
||
register_action(editor, window, Editor::scroll_cursor_bottom);
|
||
register_action(editor, window, Editor::scroll_cursor_center_top_bottom);
|
||
register_action(editor, window, |editor, _: &LineDown, window, cx| {
|
||
editor.scroll_screen(&ScrollAmount::Line(1.), window, cx)
|
||
});
|
||
register_action(editor, window, |editor, _: &LineUp, window, cx| {
|
||
editor.scroll_screen(&ScrollAmount::Line(-1.), window, cx)
|
||
});
|
||
register_action(editor, window, |editor, _: &HalfPageDown, window, cx| {
|
||
editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx)
|
||
});
|
||
register_action(editor, window, |editor, _: &HalfPageUp, window, cx| {
|
||
editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx)
|
||
});
|
||
register_action(editor, window, |editor, _: &PageDown, window, cx| {
|
||
editor.scroll_screen(&ScrollAmount::Page(1.), window, cx)
|
||
});
|
||
register_action(editor, window, |editor, _: &PageUp, window, cx| {
|
||
editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx)
|
||
});
|
||
register_action(editor, window, Editor::move_to_previous_word_start);
|
||
register_action(editor, window, Editor::move_to_previous_subword_start);
|
||
register_action(editor, window, Editor::move_to_next_word_end);
|
||
register_action(editor, window, Editor::move_to_next_subword_end);
|
||
register_action(editor, window, Editor::move_to_beginning_of_line);
|
||
register_action(editor, window, Editor::move_to_end_of_line);
|
||
register_action(editor, window, Editor::move_to_start_of_paragraph);
|
||
register_action(editor, window, Editor::move_to_end_of_paragraph);
|
||
register_action(editor, window, Editor::move_to_beginning);
|
||
register_action(editor, window, Editor::move_to_end);
|
||
register_action(editor, window, Editor::move_to_start_of_excerpt);
|
||
register_action(editor, window, Editor::move_to_start_of_next_excerpt);
|
||
register_action(editor, window, Editor::move_to_end_of_excerpt);
|
||
register_action(editor, window, Editor::move_to_end_of_previous_excerpt);
|
||
register_action(editor, window, Editor::select_up);
|
||
register_action(editor, window, Editor::select_down);
|
||
register_action(editor, window, Editor::select_left);
|
||
register_action(editor, window, Editor::select_right);
|
||
register_action(editor, window, Editor::select_to_previous_word_start);
|
||
register_action(editor, window, Editor::select_to_previous_subword_start);
|
||
register_action(editor, window, Editor::select_to_next_word_end);
|
||
register_action(editor, window, Editor::select_to_next_subword_end);
|
||
register_action(editor, window, Editor::select_to_beginning_of_line);
|
||
register_action(editor, window, Editor::select_to_end_of_line);
|
||
register_action(editor, window, Editor::select_to_start_of_paragraph);
|
||
register_action(editor, window, Editor::select_to_end_of_paragraph);
|
||
register_action(editor, window, Editor::select_to_start_of_excerpt);
|
||
register_action(editor, window, Editor::select_to_start_of_next_excerpt);
|
||
register_action(editor, window, Editor::select_to_end_of_excerpt);
|
||
register_action(editor, window, Editor::select_to_end_of_previous_excerpt);
|
||
register_action(editor, window, Editor::select_to_beginning);
|
||
register_action(editor, window, Editor::select_to_end);
|
||
register_action(editor, window, Editor::select_all);
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor.select_all_matches(action, window, cx).log_err();
|
||
});
|
||
register_action(editor, window, Editor::select_line);
|
||
register_action(editor, window, Editor::split_selection_into_lines);
|
||
register_action(editor, window, Editor::add_selection_above);
|
||
register_action(editor, window, Editor::add_selection_below);
|
||
register_action(editor, window, Editor::insert_snippet_at_selections);
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor.select_next(action, window, cx).log_err();
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor.select_previous(action, window, cx).log_err();
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor.find_next_match(action, window, cx).log_err();
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor.find_previous_match(action, window, cx).log_err();
|
||
});
|
||
register_action(editor, window, Editor::select_larger_syntax_node);
|
||
register_action(editor, window, Editor::select_smaller_syntax_node);
|
||
register_action(editor, window, Editor::select_next_syntax_node);
|
||
register_action(editor, window, Editor::select_prev_syntax_node);
|
||
register_action(
|
||
editor,
|
||
window,
|
||
Editor::select_to_start_of_larger_syntax_node,
|
||
);
|
||
register_action(editor, window, Editor::select_to_end_of_larger_syntax_node);
|
||
register_action(editor, window, Editor::move_to_start_of_larger_syntax_node);
|
||
register_action(editor, window, Editor::move_to_end_of_larger_syntax_node);
|
||
register_action(editor, window, Editor::select_enclosing_symbol);
|
||
register_action(editor, window, Editor::move_to_enclosing_bracket);
|
||
register_action(editor, window, Editor::undo_selection);
|
||
register_action(editor, window, Editor::redo_selection);
|
||
if editor.read(cx).buffer_kind(cx) == ItemBufferKind::Multibuffer {
|
||
register_action(editor, window, Editor::expand_excerpts);
|
||
register_action(editor, window, Editor::expand_excerpts_up);
|
||
register_action(editor, window, Editor::expand_excerpts_down);
|
||
}
|
||
register_action(editor, window, Editor::go_to_diagnostic);
|
||
register_action(editor, window, Editor::go_to_prev_diagnostic);
|
||
register_action(editor, window, Editor::go_to_next_hunk);
|
||
register_action(editor, window, Editor::go_to_prev_hunk);
|
||
register_action(editor, window, Editor::go_to_next_document_highlight);
|
||
register_action(editor, window, Editor::go_to_prev_document_highlight);
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor
|
||
.go_to_definition(action, window, cx)
|
||
.detach_and_log_err(cx);
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor
|
||
.go_to_definition_split(action, window, cx)
|
||
.detach_and_log_err(cx);
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor
|
||
.go_to_declaration(action, window, cx)
|
||
.detach_and_log_err(cx);
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor
|
||
.go_to_declaration_split(action, window, cx)
|
||
.detach_and_log_err(cx);
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor
|
||
.go_to_implementation(action, window, cx)
|
||
.detach_and_log_err(cx);
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor
|
||
.go_to_implementation_split(action, window, cx)
|
||
.detach_and_log_err(cx);
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor
|
||
.go_to_type_definition(action, window, cx)
|
||
.detach_and_log_err(cx);
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
editor
|
||
.go_to_type_definition_split(action, window, cx)
|
||
.detach_and_log_err(cx);
|
||
});
|
||
register_action(editor, window, Editor::open_url);
|
||
register_action(editor, window, Editor::open_selected_filename);
|
||
register_action(editor, window, Editor::fold);
|
||
register_action(editor, window, Editor::fold_at_level);
|
||
register_action(editor, window, Editor::fold_at_level_1);
|
||
register_action(editor, window, Editor::fold_at_level_2);
|
||
register_action(editor, window, Editor::fold_at_level_3);
|
||
register_action(editor, window, Editor::fold_at_level_4);
|
||
register_action(editor, window, Editor::fold_at_level_5);
|
||
register_action(editor, window, Editor::fold_at_level_6);
|
||
register_action(editor, window, Editor::fold_at_level_7);
|
||
register_action(editor, window, Editor::fold_at_level_8);
|
||
register_action(editor, window, Editor::fold_at_level_9);
|
||
register_action(editor, window, Editor::fold_all);
|
||
register_action(editor, window, Editor::fold_function_bodies);
|
||
register_action(editor, window, Editor::fold_recursive);
|
||
register_action(editor, window, Editor::toggle_fold);
|
||
register_action(editor, window, Editor::toggle_fold_recursive);
|
||
register_action(editor, window, Editor::toggle_fold_all);
|
||
register_action(editor, window, Editor::unfold_lines);
|
||
register_action(editor, window, Editor::unfold_recursive);
|
||
register_action(editor, window, Editor::unfold_all);
|
||
register_action(editor, window, Editor::fold_selected_ranges);
|
||
register_action(editor, window, Editor::set_mark);
|
||
register_action(editor, window, Editor::save_location);
|
||
register_action(editor, window, Editor::swap_selection_ends);
|
||
register_action(editor, window, Editor::show_completions);
|
||
register_action(editor, window, Editor::show_word_completions);
|
||
register_action(editor, window, Editor::toggle_code_actions);
|
||
register_action(editor, window, Editor::open_excerpts);
|
||
register_action(editor, window, Editor::open_excerpts_in_split);
|
||
register_action(editor, window, Editor::toggle_soft_wrap);
|
||
register_action(editor, window, Editor::toggle_tab_bar);
|
||
register_action(editor, window, Editor::toggle_breadcrumb);
|
||
register_action(editor, window, Editor::toggle_line_numbers);
|
||
register_action(editor, window, Editor::toggle_relative_line_numbers);
|
||
register_action(editor, window, Editor::toggle_indent_guides);
|
||
register_action(editor, window, Editor::toggle_inlay_hints);
|
||
register_action(editor, window, Editor::toggle_code_lens_action);
|
||
register_action(editor, window, Editor::toggle_semantic_highlights);
|
||
register_action(editor, window, Editor::toggle_edit_predictions);
|
||
if editor.read(cx).diagnostics_enabled() {
|
||
register_action(editor, window, Editor::toggle_diagnostics);
|
||
}
|
||
if editor.read(cx).inline_diagnostics_enabled() {
|
||
register_action(editor, window, Editor::toggle_inline_diagnostics);
|
||
}
|
||
if editor.read(cx).supports_minimap(cx) {
|
||
register_action(editor, window, Editor::toggle_minimap);
|
||
}
|
||
register_action(editor, window, hover_popover::hover);
|
||
register_action(editor, window, Editor::reveal_in_finder);
|
||
register_action(editor, window, Editor::copy_path);
|
||
register_action(editor, window, Editor::copy_relative_path);
|
||
register_action(editor, window, Editor::copy_file_name);
|
||
register_action(editor, window, Editor::copy_file_name_without_extension);
|
||
register_action(editor, window, Editor::copy_highlight_json);
|
||
register_action(editor, window, Editor::copy_permalink_to_line);
|
||
register_action(editor, window, Editor::open_permalink_to_line);
|
||
register_action(editor, window, Editor::copy_file_location);
|
||
register_action(editor, window, Editor::toggle_git_blame);
|
||
register_action(editor, window, Editor::toggle_git_blame_inline);
|
||
register_action(editor, window, Editor::open_git_blame_commit);
|
||
register_action(editor, window, Editor::toggle_selected_diff_hunks);
|
||
register_action(editor, window, Editor::toggle_staged_selected_diff_hunks);
|
||
register_action(editor, window, Editor::stage_and_next);
|
||
register_action(editor, window, Editor::unstage_and_next);
|
||
register_action(editor, window, Editor::expand_all_diff_hunks);
|
||
register_action(editor, window, Editor::collapse_all_diff_hunks);
|
||
register_action(editor, window, Editor::toggle_all_diff_hunks);
|
||
register_action(editor, window, Editor::toggle_review_comments_expanded);
|
||
register_action(editor, window, Editor::submit_diff_review_comment_action);
|
||
register_action(editor, window, Editor::edit_review_comment);
|
||
register_action(editor, window, Editor::delete_review_comment);
|
||
register_action(editor, window, Editor::confirm_edit_review_comment_action);
|
||
register_action(editor, window, Editor::cancel_edit_review_comment_action);
|
||
register_action(editor, window, Editor::go_to_previous_change);
|
||
register_action(editor, window, Editor::go_to_next_change);
|
||
register_action(editor, window, Editor::go_to_prev_reference);
|
||
register_action(editor, window, Editor::go_to_next_reference);
|
||
register_action(editor, window, Editor::go_to_previous_symbol);
|
||
register_action(editor, window, Editor::go_to_next_symbol);
|
||
register_action(editor, window, Editor::restart_language_server);
|
||
register_action(editor, window, Editor::stop_language_server);
|
||
register_action(editor, window, Editor::show_character_palette);
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.compose_completion(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.find_all_references(action, window, cx) {
|
||
task.detach_and_log_err(cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
register_action(editor, window, Editor::show_signature_help);
|
||
register_action(editor, window, Editor::signature_help_prev);
|
||
register_action(editor, window, Editor::signature_help_next);
|
||
register_action(editor, window, Editor::show_edit_prediction);
|
||
register_action(editor, window, Editor::context_menu_first);
|
||
register_action(editor, window, Editor::context_menu_prev);
|
||
register_action(editor, window, Editor::context_menu_next);
|
||
register_action(editor, window, Editor::context_menu_last);
|
||
register_action(editor, window, Editor::display_cursor_names);
|
||
register_action(editor, window, Editor::open_active_item_in_terminal);
|
||
register_action(editor, window, Editor::spawn_nearest_task);
|
||
register_action(editor, window, Editor::open_selections_in_multibuffer);
|
||
register_action(editor, window, Editor::toggle_bookmark);
|
||
register_action(editor, window, Editor::go_to_next_bookmark);
|
||
register_action(editor, window, Editor::go_to_previous_bookmark);
|
||
register_action(editor, window, Editor::toggle_breakpoint);
|
||
register_action(editor, window, Editor::edit_log_breakpoint);
|
||
register_action(editor, window, Editor::enable_breakpoint);
|
||
register_action(editor, window, Editor::disable_breakpoint);
|
||
register_action(editor, window, Editor::toggle_read_only);
|
||
register_action(editor, window, Editor::reload_file);
|
||
|
||
if !editor.read(cx).read_only(cx) {
|
||
register_action(editor, window, Editor::newline);
|
||
register_action(editor, window, Editor::newline_above);
|
||
register_action(editor, window, Editor::newline_below);
|
||
register_action(editor, window, Editor::backspace);
|
||
register_action(editor, window, Editor::delete);
|
||
register_action(editor, window, Editor::tab);
|
||
register_action(editor, window, Editor::backtab);
|
||
register_action(editor, window, Editor::indent);
|
||
register_action(editor, window, Editor::outdent);
|
||
register_action(editor, window, Editor::autoindent);
|
||
register_action(editor, window, Editor::delete_line);
|
||
register_action(editor, window, Editor::join_lines);
|
||
register_action(editor, window, Editor::sort_lines_by_length);
|
||
register_action(editor, window, Editor::sort_lines_case_sensitive);
|
||
register_action(editor, window, Editor::sort_lines_case_insensitive);
|
||
register_action(editor, window, Editor::unique_lines_case_insensitive);
|
||
register_action(editor, window, Editor::unique_lines_case_sensitive);
|
||
register_action(editor, window, Editor::reverse_lines);
|
||
register_action(editor, window, Editor::shuffle_lines);
|
||
register_action(editor, window, Editor::rotate_selections_forward);
|
||
register_action(editor, window, Editor::rotate_selections_backward);
|
||
register_action(editor, window, Editor::convert_indentation_to_spaces);
|
||
register_action(editor, window, Editor::convert_indentation_to_tabs);
|
||
register_action(editor, window, Editor::convert_to_upper_case);
|
||
register_action(editor, window, Editor::convert_to_lower_case);
|
||
register_action(editor, window, Editor::convert_to_title_case);
|
||
register_action(editor, window, Editor::convert_to_snake_case);
|
||
register_action(editor, window, Editor::convert_to_kebab_case);
|
||
register_action(editor, window, Editor::convert_to_upper_camel_case);
|
||
register_action(editor, window, Editor::convert_to_lower_camel_case);
|
||
register_action(editor, window, Editor::convert_to_opposite_case);
|
||
register_action(editor, window, Editor::convert_to_sentence_case);
|
||
register_action(editor, window, Editor::toggle_case);
|
||
register_action(editor, window, Editor::convert_to_rot13);
|
||
register_action(editor, window, Editor::convert_to_rot47);
|
||
register_action(editor, window, Editor::convert_to_base64);
|
||
register_action(editor, window, Editor::convert_from_base64);
|
||
register_action(editor, window, Editor::delete_to_previous_word_start);
|
||
register_action(editor, window, Editor::delete_to_previous_subword_start);
|
||
register_action(editor, window, Editor::delete_to_next_word_end);
|
||
register_action(editor, window, Editor::delete_to_next_subword_end);
|
||
register_action(editor, window, Editor::delete_to_beginning_of_line);
|
||
register_action(editor, window, Editor::delete_to_end_of_line);
|
||
register_action(editor, window, Editor::cut_to_end_of_line);
|
||
register_action(editor, window, Editor::duplicate_line_up);
|
||
register_action(editor, window, Editor::duplicate_line_down);
|
||
register_action(editor, window, Editor::duplicate_selection);
|
||
register_action(editor, window, Editor::move_line_up);
|
||
register_action(editor, window, Editor::move_line_down);
|
||
register_action(editor, window, Editor::transpose);
|
||
register_action(editor, window, |editor, _: &crate::Rewrap, _, cx| {
|
||
editor.rewrap(crate::RewrapOptions::default(), cx);
|
||
});
|
||
register_action(editor, window, Editor::cut);
|
||
register_action(editor, window, Editor::kill_ring_cut);
|
||
register_action(editor, window, Editor::kill_ring_yank);
|
||
register_action(editor, window, Editor::paste);
|
||
register_action(editor, window, Editor::undo);
|
||
register_action(editor, window, Editor::redo);
|
||
register_action(editor, window, Editor::toggle_comments);
|
||
register_action(editor, window, Editor::toggle_block_comments);
|
||
register_action(editor, window, Editor::unwrap_syntax_node);
|
||
register_action(editor, window, Editor::accept_next_word_edit_prediction);
|
||
register_action(editor, window, Editor::accept_next_line_edit_prediction);
|
||
register_action(editor, window, Editor::accept_edit_prediction);
|
||
register_action(editor, window, Editor::restore_file);
|
||
register_action(editor, window, Editor::git_restore);
|
||
register_action(editor, window, Editor::restore_and_next);
|
||
register_action(editor, window, Editor::apply_all_diff_hunks);
|
||
register_action(editor, window, Editor::apply_selected_diff_hunks);
|
||
register_action(editor, window, Editor::insert_uuid_v4);
|
||
register_action(editor, window, Editor::insert_uuid_v7);
|
||
register_action(editor, window, Editor::align_selections);
|
||
if editor.read(cx).enable_wrap_selections_in_tag(cx) {
|
||
register_action(editor, window, Editor::wrap_selections_in_tag);
|
||
}
|
||
register_action(
|
||
editor,
|
||
window,
|
||
|editor, HandleInput(text): &HandleInput, window, cx| {
|
||
if text.is_empty() {
|
||
return;
|
||
}
|
||
editor.handle_input(text, window, cx);
|
||
},
|
||
);
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.format(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
if editor.read(cx).can_format_selections(cx) {
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.format_selections(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
}
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.organize_imports(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.confirm_completion(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.confirm_completion_replace(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.confirm_completion_insert(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.confirm_code_action(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.rename(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
register_action(editor, window, |editor, action, window, cx| {
|
||
if let Some(task) = editor.confirm_rename(action, window, cx) {
|
||
editor.detach_and_notify_err(task, window, cx);
|
||
} else {
|
||
cx.propagate();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {
|
||
let position_map = layout.position_map.clone();
|
||
window.on_key_event({
|
||
let editor = self.editor.clone();
|
||
move |event: &ModifiersChangedEvent, phase, window, cx| {
|
||
if phase != DispatchPhase::Bubble {
|
||
return;
|
||
}
|
||
editor.update(cx, |editor, cx| {
|
||
let inlay_hint_settings = inlay_hint_settings(
|
||
editor.selections.newest_anchor().head(),
|
||
&editor.buffer.read(cx).snapshot(cx),
|
||
cx,
|
||
);
|
||
|
||
if let Some(inlay_modifiers) = inlay_hint_settings
|
||
.toggle_on_modifiers_press
|
||
.as_ref()
|
||
.filter(|modifiers| modifiers.modified())
|
||
{
|
||
editor.refresh_inlay_hints(
|
||
InlayHintRefreshReason::ModifiersChanged(
|
||
inlay_modifiers == &event.modifiers,
|
||
),
|
||
cx,
|
||
);
|
||
}
|
||
|
||
if editor.hover_state.focused(window, cx) {
|
||
return;
|
||
}
|
||
|
||
editor.handle_modifiers_changed(event.modifiers, &position_map, window, cx);
|
||
})
|
||
}
|
||
});
|
||
}
|
||
|
||
fn layout_selections(
|
||
&self,
|
||
start_anchor: Anchor,
|
||
end_anchor: Anchor,
|
||
local_selections: &[Selection<Point>],
|
||
snapshot: &EditorSnapshot,
|
||
start_row: DisplayRow,
|
||
end_row: DisplayRow,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> (
|
||
Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||
BTreeMap<DisplayRow, LineHighlightSpec>,
|
||
Option<DisplayPoint>,
|
||
) {
|
||
let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
|
||
let mut active_rows = BTreeMap::new();
|
||
let mut newest_selection_head = None;
|
||
|
||
let Some(editor_with_selections) = self.editor_with_selections(cx) else {
|
||
return (selections, active_rows, newest_selection_head);
|
||
};
|
||
|
||
editor_with_selections.update(cx, |editor, cx| {
|
||
if editor.show_local_selections {
|
||
let mut layouts = Vec::new();
|
||
let newest = editor.selections.newest(&editor.display_snapshot(cx));
|
||
for selection in local_selections.iter().cloned() {
|
||
let is_empty = selection.start == selection.end;
|
||
let is_newest = selection == newest;
|
||
|
||
let layout = SelectionLayout::new(
|
||
selection,
|
||
editor.selections.line_mode(),
|
||
editor.cursor_offset_on_selection,
|
||
editor.cursor_shape,
|
||
&snapshot.display_snapshot,
|
||
is_newest,
|
||
editor.leader_id.is_none(),
|
||
None,
|
||
);
|
||
if is_newest {
|
||
newest_selection_head = Some(layout.head);
|
||
}
|
||
|
||
for row in cmp::max(layout.active_rows.start.0, start_row.0)
|
||
..=cmp::min(layout.active_rows.end.0, end_row.0)
|
||
{
|
||
let contains_non_empty_selection = active_rows
|
||
.entry(DisplayRow(row))
|
||
.or_insert_with(LineHighlightSpec::default);
|
||
contains_non_empty_selection.selection |= !is_empty;
|
||
}
|
||
layouts.push(layout);
|
||
}
|
||
|
||
let mut player = editor.current_user_player_color(cx);
|
||
if !editor.is_focused(window) {
|
||
const UNFOCUS_EDITOR_SELECTION_OPACITY: f32 = 0.5;
|
||
player.selection = player.selection.opacity(UNFOCUS_EDITOR_SELECTION_OPACITY);
|
||
}
|
||
selections.push((player, layouts));
|
||
|
||
if let SelectionDragState::Dragging {
|
||
ref selection,
|
||
ref drop_cursor,
|
||
ref hide_drop_cursor,
|
||
} = editor.selection_drag_state
|
||
&& !hide_drop_cursor
|
||
&& (drop_cursor
|
||
.start
|
||
.cmp(&selection.start, &snapshot.buffer_snapshot())
|
||
.eq(&Ordering::Less)
|
||
|| drop_cursor
|
||
.end
|
||
.cmp(&selection.end, &snapshot.buffer_snapshot())
|
||
.eq(&Ordering::Greater))
|
||
{
|
||
let drag_cursor_layout = SelectionLayout::new(
|
||
drop_cursor.clone(),
|
||
false,
|
||
editor.cursor_offset_on_selection,
|
||
CursorShape::Bar,
|
||
&snapshot.display_snapshot,
|
||
false,
|
||
false,
|
||
None,
|
||
);
|
||
let absent_color = cx.theme().players().absent();
|
||
selections.push((absent_color, vec![drag_cursor_layout]));
|
||
}
|
||
}
|
||
|
||
if let Some(collaboration_hub) = &editor.collaboration_hub {
|
||
// When following someone, render the local selections in their color.
|
||
if let Some(leader_id) = editor.leader_id {
|
||
match leader_id {
|
||
CollaboratorId::PeerId(peer_id) => {
|
||
if let Some(collaborator) =
|
||
collaboration_hub.collaborators(cx).get(&peer_id)
|
||
&& let Some(participant_index) = collaboration_hub
|
||
.user_participant_indices(cx)
|
||
.get(&collaborator.user_id)
|
||
&& let Some((local_selection_style, _)) = selections.first_mut()
|
||
{
|
||
*local_selection_style = cx
|
||
.theme()
|
||
.players()
|
||
.color_for_participant(participant_index.0);
|
||
}
|
||
}
|
||
CollaboratorId::Agent => {
|
||
if let Some((local_selection_style, _)) = selections.first_mut() {
|
||
*local_selection_style = cx.theme().players().agent();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let mut remote_selections = HashMap::default();
|
||
for selection in snapshot.remote_selections_in_range(
|
||
&(start_anchor..end_anchor),
|
||
collaboration_hub.as_ref(),
|
||
cx,
|
||
) {
|
||
// Don't re-render the leader's selections, since the local selections
|
||
// match theirs.
|
||
if Some(selection.collaborator_id) == editor.leader_id {
|
||
continue;
|
||
}
|
||
let key = HoveredCursor {
|
||
replica_id: selection.replica_id,
|
||
selection_id: selection.selection.id,
|
||
};
|
||
|
||
let is_shown =
|
||
editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
|
||
|
||
remote_selections
|
||
.entry(selection.replica_id)
|
||
.or_insert((selection.color, Vec::new()))
|
||
.1
|
||
.push(SelectionLayout::new(
|
||
selection.selection,
|
||
selection.line_mode,
|
||
editor.cursor_offset_on_selection,
|
||
selection.cursor_shape,
|
||
&snapshot.display_snapshot,
|
||
false,
|
||
false,
|
||
if is_shown { selection.user_name } else { None },
|
||
));
|
||
}
|
||
|
||
selections.extend(remote_selections.into_values());
|
||
} else if !editor.is_focused(window) && editor.show_cursor_when_unfocused {
|
||
let cursor_offset_on_selection = editor.cursor_offset_on_selection;
|
||
|
||
let layouts = snapshot
|
||
.buffer_snapshot()
|
||
.selections_in_range(&(start_anchor..end_anchor), true)
|
||
.map(move |(_, line_mode, cursor_shape, selection)| {
|
||
SelectionLayout::new(
|
||
selection,
|
||
line_mode,
|
||
cursor_offset_on_selection,
|
||
cursor_shape,
|
||
&snapshot.display_snapshot,
|
||
false,
|
||
false,
|
||
None,
|
||
)
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let player = editor.current_user_player_color(cx);
|
||
selections.push((player, layouts));
|
||
}
|
||
});
|
||
|
||
#[cfg(debug_assertions)]
|
||
Self::layout_debug_ranges(
|
||
&mut selections,
|
||
start_anchor..end_anchor,
|
||
&snapshot.display_snapshot,
|
||
cx,
|
||
);
|
||
|
||
(selections, active_rows, newest_selection_head)
|
||
}
|
||
|
||
fn collect_cursors(
|
||
&self,
|
||
snapshot: &EditorSnapshot,
|
||
cx: &mut App,
|
||
) -> Vec<(DisplayPoint, Hsla)> {
|
||
let editor = self.editor.read(cx);
|
||
let mut cursors = Vec::new();
|
||
let mut skip_local = false;
|
||
let mut add_cursor = |anchor: Anchor, color| {
|
||
cursors.push((anchor.to_display_point(&snapshot.display_snapshot), color));
|
||
};
|
||
// Remote cursors
|
||
if let Some(collaboration_hub) = &editor.collaboration_hub {
|
||
for remote_selection in snapshot.remote_selections_in_range(
|
||
&(Anchor::Min..Anchor::Max),
|
||
collaboration_hub.deref(),
|
||
cx,
|
||
) {
|
||
add_cursor(
|
||
remote_selection.selection.head(),
|
||
remote_selection.color.cursor,
|
||
);
|
||
if Some(remote_selection.collaborator_id) == editor.leader_id {
|
||
skip_local = true;
|
||
}
|
||
}
|
||
}
|
||
// Local cursors
|
||
if !skip_local {
|
||
let color = cx.theme().players().local().cursor;
|
||
editor
|
||
.selections
|
||
.disjoint_anchors()
|
||
.iter()
|
||
.for_each(|selection| {
|
||
add_cursor(selection.head(), color);
|
||
});
|
||
if let Some(ref selection) = editor.selections.pending_anchor() {
|
||
add_cursor(selection.head(), color);
|
||
}
|
||
}
|
||
cursors
|
||
}
|
||
|
||
fn layout_visible_cursors(
|
||
&self,
|
||
snapshot: &EditorSnapshot,
|
||
selections: &[(PlayerColor, Vec<SelectionLayout>)],
|
||
row_block_types: &HashMap<DisplayRow, bool>,
|
||
visible_display_row_range: Range<DisplayRow>,
|
||
line_layouts: &[LineWithInvisibles],
|
||
text_hitbox: &Hitbox,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
line_height: Pixels,
|
||
em_width: Pixels,
|
||
em_advance: Pixels,
|
||
autoscroll_containing_element: bool,
|
||
redacted_ranges: &[Range<DisplayPoint>],
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<CursorLayout> {
|
||
let mut autoscroll_bounds = None;
|
||
let cursor_layouts = self.editor.update(cx, |editor, cx| {
|
||
let mut cursors = Vec::new();
|
||
|
||
let show_local_cursors = editor.show_local_cursors(window, cx);
|
||
|
||
for (player_color, selections) in selections {
|
||
for selection in selections {
|
||
let cursor_position = selection.head;
|
||
|
||
let in_range = visible_display_row_range.contains(&cursor_position.row());
|
||
if (selection.is_local && !show_local_cursors)
|
||
|| !in_range
|
||
|| row_block_types.get(&cursor_position.row()) == Some(&true)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
let cursor_row_layout = &line_layouts
|
||
[cursor_position.row().minus(visible_display_row_range.start) as usize];
|
||
let cursor_column = cursor_position.column() as usize;
|
||
|
||
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column)
|
||
+ cursor_row_layout
|
||
.alignment_offset(self.style.text.text_align, text_hitbox.size.width);
|
||
let cursor_next_x = cursor_row_layout.x_for_index(cursor_column + 1)
|
||
+ cursor_row_layout
|
||
.alignment_offset(self.style.text.text_align, text_hitbox.size.width);
|
||
let mut cell_width = cursor_next_x - cursor_character_x;
|
||
if cell_width == Pixels::ZERO {
|
||
cell_width = em_advance;
|
||
}
|
||
|
||
let mut block_width = cell_width;
|
||
let mut block_text = None;
|
||
|
||
let is_cursor_in_redacted_range = redacted_ranges
|
||
.iter()
|
||
.any(|range| range.start <= cursor_position && cursor_position < range.end);
|
||
|
||
if selection.cursor_shape == CursorShape::Block && !is_cursor_in_redacted_range
|
||
{
|
||
if let Some(text) = snapshot.grapheme_at(cursor_position).or_else(|| {
|
||
if snapshot.is_empty() {
|
||
snapshot.placeholder_text().and_then(|s| {
|
||
s.graphemes(true).next().map(|s| s.to_string().into())
|
||
})
|
||
} else {
|
||
None
|
||
}
|
||
}) {
|
||
let is_ascii_whitespace_only =
|
||
text.as_ref().chars().all(|c| c.is_ascii_whitespace());
|
||
let len = text.len();
|
||
|
||
let mut font = cursor_row_layout
|
||
.font_id_for_index(cursor_column)
|
||
.and_then(|cursor_font_id| {
|
||
window.text_system().get_font_for_id(cursor_font_id)
|
||
})
|
||
.unwrap_or(self.style.text.font());
|
||
font.features = self.style.text.font_features.clone();
|
||
|
||
// Invert the text color for the block cursor. Ensure that the text
|
||
// color is opaque enough to be visible against the background color.
|
||
//
|
||
// 0.75 is an arbitrary threshold to determine if the background color is
|
||
// opaque enough to use as a text color.
|
||
//
|
||
// TODO: In the future we should ensure themes have a `text_inverse` color.
|
||
let color = if cx.theme().colors().editor_background.a < 0.75 {
|
||
match cx.theme().appearance {
|
||
Appearance::Dark => Hsla::black(),
|
||
Appearance::Light => Hsla::white(),
|
||
}
|
||
} else {
|
||
cx.theme().colors().editor_background
|
||
};
|
||
|
||
let shaped = window.text_system().shape_line(
|
||
text,
|
||
cursor_row_layout.font_size,
|
||
&[TextRun {
|
||
len,
|
||
font,
|
||
color,
|
||
..Default::default()
|
||
}],
|
||
None,
|
||
);
|
||
if !is_ascii_whitespace_only {
|
||
block_width = block_width.max(shaped.width);
|
||
}
|
||
block_text = Some(shaped);
|
||
}
|
||
}
|
||
|
||
let x = cursor_character_x - scroll_pixel_position.x.into();
|
||
let y = ((cursor_position.row().as_f64() - scroll_position.y)
|
||
* ScrollPixelOffset::from(line_height))
|
||
.into();
|
||
if selection.is_newest {
|
||
editor.pixel_position_of_newest_cursor = Some(point(
|
||
text_hitbox.origin.x + x + block_width / 2.,
|
||
text_hitbox.origin.y + y + line_height / 2.,
|
||
));
|
||
|
||
if autoscroll_containing_element {
|
||
let top = text_hitbox.origin.y
|
||
+ ((cursor_position.row().as_f64() - scroll_position.y - 3.)
|
||
.max(0.)
|
||
* ScrollPixelOffset::from(line_height))
|
||
.into();
|
||
let left = text_hitbox.origin.x
|
||
+ ((cursor_position.column() as ScrollOffset
|
||
- scroll_position.x
|
||
- 3.)
|
||
.max(0.)
|
||
* ScrollPixelOffset::from(em_width))
|
||
.into();
|
||
|
||
let bottom = text_hitbox.origin.y
|
||
+ ((cursor_position.row().as_f64() - scroll_position.y + 4.)
|
||
* ScrollPixelOffset::from(line_height))
|
||
.into();
|
||
let right = text_hitbox.origin.x
|
||
+ ((cursor_position.column() as ScrollOffset - scroll_position.x
|
||
+ 4.)
|
||
* ScrollPixelOffset::from(em_width))
|
||
.into();
|
||
|
||
autoscroll_bounds =
|
||
Some(Bounds::from_corners(point(left, top), point(right, bottom)))
|
||
}
|
||
}
|
||
|
||
let mut cursor = CursorLayout {
|
||
color: player_color.cursor,
|
||
block_width,
|
||
origin: point(x, y),
|
||
line_height,
|
||
shape: selection.cursor_shape,
|
||
block_text,
|
||
cursor_name: None,
|
||
};
|
||
let cursor_name = selection.user_name.clone().map(|name| CursorName {
|
||
string: name,
|
||
color: self.style.background,
|
||
is_top_row: cursor_position.row().0 == 0,
|
||
});
|
||
cursor.layout(content_origin, cursor_name, window, cx);
|
||
cursors.push(cursor);
|
||
}
|
||
}
|
||
|
||
cursors
|
||
});
|
||
|
||
if let Some(bounds) = autoscroll_bounds {
|
||
window.request_autoscroll(bounds);
|
||
}
|
||
|
||
cursor_layouts
|
||
}
|
||
|
||
fn layout_navigation_overlays(
|
||
&self,
|
||
snapshot: &EditorSnapshot,
|
||
visible_display_row_range: Range<DisplayRow>,
|
||
line_layouts: &[LineWithInvisibles],
|
||
text_hitbox: &Hitbox,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
line_height: Pixels,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<NavigationOverlayPaintCommand> {
|
||
let mut overlay_sets = self
|
||
.editor
|
||
.read(cx)
|
||
.navigation_overlay_sets()
|
||
.iter()
|
||
.map(|(key, overlays)| (*key, overlays.clone()))
|
||
.collect::<Vec<_>>();
|
||
if overlay_sets.is_empty() {
|
||
return Vec::new();
|
||
}
|
||
overlay_sets.sort_by_key(|(key, _)| *key);
|
||
|
||
let layout_context = NavigationOverlayLayoutContext {
|
||
display_snapshot: &snapshot.display_snapshot,
|
||
visible_display_row_range: &visible_display_row_range,
|
||
line_layouts,
|
||
text_align: self.style.text.text_align,
|
||
content_width: text_hitbox.size.width,
|
||
content_origin,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
line_height,
|
||
editor_font: self.style.text.font(),
|
||
editor_font_size: self.style.text.font_size.to_pixels(window.rem_size()),
|
||
};
|
||
let mut navigation_overlay_paint_commands = Vec::new();
|
||
|
||
for (_, overlays) in overlay_sets {
|
||
for overlay in overlays.as_ref() {
|
||
Self::layout_navigation_label(
|
||
overlay,
|
||
&layout_context,
|
||
window,
|
||
cx,
|
||
&mut navigation_overlay_paint_commands,
|
||
);
|
||
}
|
||
}
|
||
|
||
navigation_overlay_paint_commands
|
||
}
|
||
|
||
fn layout_navigation_label(
|
||
overlay: &crate::NavigationTargetOverlay,
|
||
context: &NavigationOverlayLayoutContext<'_>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
paint_commands: &mut Vec<NavigationOverlayPaintCommand>,
|
||
) {
|
||
let label = &overlay.label;
|
||
let label_display_point = overlay
|
||
.target_range
|
||
.start
|
||
.to_display_point(context.display_snapshot);
|
||
let label_row = label_display_point.row();
|
||
if !context.visible_display_row_range.contains(&label_row) {
|
||
return;
|
||
}
|
||
|
||
let row_index = label_row.minus(context.visible_display_row_range.start) as usize;
|
||
let row_layout = &context.line_layouts[row_index];
|
||
let label_column = label_display_point.column().min(row_layout.len as u32) as usize;
|
||
let label_x = row_layout.x_for_index(label_column)
|
||
+ row_layout.alignment_offset(context.text_align, context.content_width)
|
||
- context.scroll_pixel_position.x.into()
|
||
+ label.x_offset;
|
||
let label_y = ((label_row.as_f64() - context.scroll_position.y)
|
||
* ScrollPixelOffset::from(context.line_height))
|
||
.into();
|
||
let label_text_size = (context.editor_font_size * label.scale_factor.max(0.0)).max(px(1.0));
|
||
let origin = context.content_origin + point(label_x, label_y);
|
||
|
||
let mut element = div()
|
||
.block_mouse_except_scroll()
|
||
.font(context.editor_font.clone())
|
||
.text_size(label_text_size)
|
||
.text_color(label.text_color)
|
||
.line_height(context.line_height)
|
||
.child(label.text.clone())
|
||
.into_any_element();
|
||
element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx);
|
||
|
||
paint_commands.push(NavigationOverlayPaintCommand::Label(
|
||
NavigationLabelLayout { element, origin },
|
||
));
|
||
}
|
||
|
||
fn layout_scrollbars(
|
||
&self,
|
||
snapshot: &EditorSnapshot,
|
||
scrollbar_layout_information: &ScrollbarLayoutInformation,
|
||
content_offset: gpui::Point<Pixels>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
non_visible_cursors: bool,
|
||
right_margin: Pixels,
|
||
editor_width: Pixels,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<EditorScrollbars> {
|
||
let show_scrollbars = self.editor.read(cx).show_scrollbars;
|
||
if (!show_scrollbars.horizontal && !show_scrollbars.vertical)
|
||
|| self.style.scrollbar_width.is_zero()
|
||
{
|
||
return None;
|
||
}
|
||
|
||
// If a drag took place after we started dragging the scrollbar,
|
||
// cancel the scrollbar drag.
|
||
if cx.has_active_drag() {
|
||
self.editor.update(cx, |editor, cx| {
|
||
editor.scroll_manager.reset_scrollbar_state(cx)
|
||
});
|
||
}
|
||
|
||
let editor_settings = EditorSettings::get_global(cx);
|
||
let scrollbar_settings = editor_settings.scrollbar;
|
||
let show_scrollbars = match scrollbar_settings.show {
|
||
ShowScrollbar::Auto => {
|
||
let editor = self.editor.read(cx);
|
||
let is_singleton = editor.buffer_kind(cx) == ItemBufferKind::Singleton;
|
||
// Git
|
||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot().has_diff_hunks())
|
||
||
|
||
// Buffer Search Results
|
||
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights(HighlightKey::BufferSearchHighlights))
|
||
||
|
||
// Selected Text Occurrences
|
||
(is_singleton && scrollbar_settings.selected_text && editor.has_background_highlights(HighlightKey::SelectedTextHighlight))
|
||
||
|
||
// Selected Symbol Occurrences
|
||
(is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights(HighlightKey::DocumentHighlightRead) || editor.has_background_highlights(HighlightKey::DocumentHighlightWrite)))
|
||
||
|
||
// Diagnostics
|
||
(is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot().has_diagnostics())
|
||
||
|
||
// Cursors out of sight
|
||
non_visible_cursors
|
||
||
|
||
// Scrollmanager
|
||
editor.scroll_manager.scrollbars_visible()
|
||
}
|
||
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
|
||
ShowScrollbar::Always => true,
|
||
ShowScrollbar::Never => return None,
|
||
};
|
||
|
||
// The horizontal scrollbar is usually slightly offset to align nicely with
|
||
// indent guides. However, this offset is not needed if indent guides are
|
||
// disabled for the current editor.
|
||
let content_offset = self
|
||
.editor
|
||
.read(cx)
|
||
.show_indent_guides
|
||
.is_none_or(|should_show| should_show)
|
||
.then_some(content_offset)
|
||
.unwrap_or_default();
|
||
|
||
Some(EditorScrollbars::from_scrollbar_axes(
|
||
ScrollbarAxes {
|
||
horizontal: scrollbar_settings.axes.horizontal
|
||
&& self.editor.read(cx).show_scrollbars.horizontal,
|
||
vertical: scrollbar_settings.axes.vertical
|
||
&& self.editor.read(cx).show_scrollbars.vertical,
|
||
},
|
||
scrollbar_layout_information,
|
||
content_offset,
|
||
scroll_position,
|
||
self.style.scrollbar_width,
|
||
right_margin,
|
||
editor_width,
|
||
show_scrollbars,
|
||
self.editor.read(cx).scroll_manager.active_scrollbar_state(),
|
||
window,
|
||
))
|
||
}
|
||
|
||
fn layout_minimap(
|
||
&self,
|
||
snapshot: &EditorSnapshot,
|
||
minimap_width: Pixels,
|
||
scroll_position: gpui::Point<f64>,
|
||
scrollbar_layout_information: &ScrollbarLayoutInformation,
|
||
scrollbar_layout: Option<&EditorScrollbars>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<MinimapLayout> {
|
||
let minimap_editor = self.editor.read(cx).minimap().cloned()?;
|
||
|
||
let minimap_settings = EditorSettings::get_global(cx).minimap;
|
||
|
||
if minimap_settings.on_active_editor() {
|
||
let active_editor = self.editor.read(cx).workspace().and_then(|ws| {
|
||
ws.read(cx)
|
||
.active_pane()
|
||
.read(cx)
|
||
.active_item()
|
||
.and_then(|i| i.act_as::<Editor>(cx))
|
||
});
|
||
if active_editor.is_some_and(|e| e != self.editor) {
|
||
return None;
|
||
}
|
||
}
|
||
|
||
if !snapshot.mode.is_full()
|
||
|| minimap_width.is_zero()
|
||
|| matches!(
|
||
minimap_settings.show,
|
||
ShowMinimap::Auto if scrollbar_layout.is_none_or(|layout| !layout.visible)
|
||
)
|
||
{
|
||
return None;
|
||
}
|
||
|
||
const MINIMAP_AXIS: ScrollbarAxis = ScrollbarAxis::Vertical;
|
||
|
||
let ScrollbarLayoutInformation {
|
||
editor_bounds,
|
||
scroll_range,
|
||
glyph_grid_cell,
|
||
} = scrollbar_layout_information;
|
||
|
||
let line_height = glyph_grid_cell.height;
|
||
let scroll_position = scroll_position.along(MINIMAP_AXIS);
|
||
|
||
let top_right_anchor = scrollbar_layout
|
||
.and_then(|layout| layout.vertical.as_ref())
|
||
.map(|vertical_scrollbar| vertical_scrollbar.hitbox.origin)
|
||
.unwrap_or_else(|| editor_bounds.top_right());
|
||
|
||
let thumb_state = self
|
||
.editor
|
||
.read_with(cx, |editor, _| editor.scroll_manager.minimap_thumb_state());
|
||
|
||
let show_thumb = match minimap_settings.thumb {
|
||
MinimapThumb::Always => true,
|
||
MinimapThumb::Hover => thumb_state.is_some(),
|
||
};
|
||
|
||
let minimap_bounds = Bounds::from_anchor_and_size(
|
||
gpui::Anchor::TopRight,
|
||
top_right_anchor,
|
||
size(minimap_width, editor_bounds.size.height),
|
||
);
|
||
let minimap_line_height = self.get_minimap_line_height(
|
||
minimap_editor
|
||
.read(cx)
|
||
.text_style_refinement
|
||
.as_ref()
|
||
.and_then(|refinement| refinement.font_size)
|
||
.unwrap_or(MINIMAP_FONT_SIZE),
|
||
window,
|
||
cx,
|
||
);
|
||
let minimap_height = minimap_bounds.size.height;
|
||
|
||
let visible_editor_lines = (editor_bounds.size.height / line_height) as f64;
|
||
let total_editor_lines = (scroll_range.height / line_height) as f64;
|
||
let minimap_lines = (minimap_height / minimap_line_height) as f64;
|
||
|
||
let minimap_scroll_top = MinimapLayout::calculate_minimap_top_offset(
|
||
total_editor_lines,
|
||
visible_editor_lines,
|
||
minimap_lines,
|
||
scroll_position,
|
||
);
|
||
|
||
let layout = ScrollbarLayout::for_minimap(
|
||
window.insert_hitbox(minimap_bounds, HitboxBehavior::Normal),
|
||
visible_editor_lines,
|
||
total_editor_lines,
|
||
minimap_line_height,
|
||
scroll_position,
|
||
minimap_scroll_top,
|
||
show_thumb,
|
||
)
|
||
.with_thumb_state(thumb_state);
|
||
|
||
minimap_editor.update(cx, |editor, cx| {
|
||
editor.set_scroll_position(point(0., minimap_scroll_top), window, cx)
|
||
});
|
||
|
||
// Required for the drop shadow to be visible
|
||
const PADDING_OFFSET: Pixels = px(4.);
|
||
|
||
let mut minimap = div()
|
||
.size_full()
|
||
.shadow_xs()
|
||
.px(PADDING_OFFSET)
|
||
.child(minimap_editor)
|
||
.into_any_element();
|
||
|
||
let extended_bounds = minimap_bounds.extend(Edges {
|
||
right: PADDING_OFFSET,
|
||
left: PADDING_OFFSET,
|
||
..Default::default()
|
||
});
|
||
minimap.layout_as_root(extended_bounds.size.into(), window, cx);
|
||
window.with_absolute_element_offset(extended_bounds.origin, |window| {
|
||
minimap.prepaint(window, cx)
|
||
});
|
||
|
||
Some(MinimapLayout {
|
||
minimap,
|
||
thumb_layout: layout,
|
||
thumb_border_style: minimap_settings.thumb_border,
|
||
minimap_line_height,
|
||
minimap_scroll_top,
|
||
max_scroll_top: total_editor_lines,
|
||
})
|
||
}
|
||
|
||
fn get_minimap_line_height(
|
||
&self,
|
||
font_size: AbsoluteLength,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Pixels {
|
||
let rem_size = self.rem_size(cx).unwrap_or(window.rem_size());
|
||
let mut text_style = self.style.text.clone();
|
||
text_style.font_size = font_size;
|
||
text_style.line_height_in_pixels(rem_size)
|
||
}
|
||
|
||
fn get_minimap_width(
|
||
&self,
|
||
minimap_settings: &Minimap,
|
||
scrollbars_shown: bool,
|
||
text_width: Pixels,
|
||
em_width: Pixels,
|
||
font_size: Pixels,
|
||
rem_size: Pixels,
|
||
cx: &App,
|
||
) -> Option<Pixels> {
|
||
if minimap_settings.show == ShowMinimap::Auto && !scrollbars_shown {
|
||
return None;
|
||
}
|
||
|
||
let minimap_font_size = self.editor.read_with(cx, |editor, cx| {
|
||
editor.minimap().map(|minimap_editor| {
|
||
minimap_editor
|
||
.read(cx)
|
||
.text_style_refinement
|
||
.as_ref()
|
||
.and_then(|refinement| refinement.font_size)
|
||
.unwrap_or(MINIMAP_FONT_SIZE)
|
||
})
|
||
})?;
|
||
|
||
let minimap_em_width = em_width * (minimap_font_size.to_pixels(rem_size) / font_size);
|
||
|
||
let minimap_width = (text_width * MinimapLayout::MINIMAP_WIDTH_PCT)
|
||
.min(minimap_em_width * minimap_settings.max_width_columns.get() as f32);
|
||
|
||
(minimap_width >= minimap_em_width * MinimapLayout::MINIMAP_MIN_WIDTH_COLUMNS)
|
||
.then_some(minimap_width)
|
||
}
|
||
|
||
fn prepaint_crease_toggles(
|
||
&self,
|
||
crease_toggles: &mut [Option<AnyElement>],
|
||
line_height: Pixels,
|
||
gutter_dimensions: &GutterDimensions,
|
||
gutter_settings: crate::editor_settings::Gutter,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
start_row: DisplayRow,
|
||
gutter_hitbox: &Hitbox,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for (ix, crease_toggle) in crease_toggles.iter_mut().enumerate() {
|
||
if let Some(crease_toggle) = crease_toggle {
|
||
debug_assert!(gutter_settings.folds);
|
||
let available_space = size(
|
||
AvailableSpace::MinContent,
|
||
AvailableSpace::Definite(line_height * 0.55),
|
||
);
|
||
let crease_toggle_size = crease_toggle.layout_as_root(available_space, window, cx);
|
||
|
||
let display_row = DisplayRow(start_row.0 + ix as u32);
|
||
let position = point(
|
||
gutter_dimensions.width - gutter_dimensions.right_padding,
|
||
line_height * (display_row.as_f64() - scroll_position.y) as f32,
|
||
);
|
||
let centering_offset = point(
|
||
(gutter_dimensions.fold_area_width() - crease_toggle_size.width) / 2.,
|
||
(line_height - crease_toggle_size.height) / 2.,
|
||
);
|
||
let origin = gutter_hitbox.origin + position + centering_offset;
|
||
crease_toggle.prepaint_as_root(origin, available_space, window, cx);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn prepaint_expand_toggles(
|
||
&self,
|
||
expand_toggles: &mut [Option<(AnyElement, gpui::Point<Pixels>)>],
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for (expand_toggle, origin) in expand_toggles.iter_mut().flatten() {
|
||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||
expand_toggle.layout_as_root(available_space, window, cx);
|
||
expand_toggle.prepaint_as_root(*origin, available_space, window, cx);
|
||
}
|
||
}
|
||
|
||
fn prepaint_crease_trailers(
|
||
&self,
|
||
trailers: Vec<Option<AnyElement>>,
|
||
lines: &[LineWithInvisibles],
|
||
line_height: Pixels,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
start_row: DisplayRow,
|
||
em_width: Pixels,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<Option<CreaseTrailerLayout>> {
|
||
trailers
|
||
.into_iter()
|
||
.enumerate()
|
||
.map(|(ix, element)| {
|
||
let mut element = element?;
|
||
let available_space = size(
|
||
AvailableSpace::MinContent,
|
||
AvailableSpace::Definite(line_height),
|
||
);
|
||
let size = element.layout_as_root(available_space, window, cx);
|
||
|
||
let line = &lines[ix];
|
||
let padding = if line.width == Pixels::ZERO {
|
||
Pixels::ZERO
|
||
} else {
|
||
4. * em_width
|
||
};
|
||
let position = point(
|
||
Pixels::from(scroll_pixel_position.x) + line.width + padding,
|
||
line_height
|
||
* (DisplayRow(start_row.0 + ix as u32).as_f64() - scroll_position.y) as f32,
|
||
);
|
||
let centering_offset = point(px(0.), (line_height - size.height) / 2.);
|
||
let origin = content_origin + position + centering_offset;
|
||
element.prepaint_as_root(origin, available_space, window, cx);
|
||
Some(CreaseTrailerLayout {
|
||
element,
|
||
bounds: Bounds::new(origin, size),
|
||
})
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
// Folds contained in a hunk are ignored apart from shrinking visual size
|
||
// If a fold contains any hunks then that fold line is marked as modified
|
||
fn layout_gutter_diff_hunks(
|
||
&self,
|
||
line_height: Pixels,
|
||
gutter_hitbox: &Hitbox,
|
||
display_rows: Range<DisplayRow>,
|
||
snapshot: &EditorSnapshot,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
|
||
let folded_buffers = self.editor.read(cx).folded_buffers(cx);
|
||
let mut display_hunks = snapshot
|
||
.display_diff_hunks_for_rows(display_rows, folded_buffers)
|
||
.map(|hunk| (hunk, None))
|
||
.collect::<Vec<_>>();
|
||
let git_gutter_setting = ProjectSettings::get_global(cx).git.git_gutter;
|
||
if let GitGutterSetting::TrackedFiles = git_gutter_setting {
|
||
for (hunk, hitbox) in &mut display_hunks {
|
||
if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
|
||
let hunk_bounds = Self::diff_hunk_bounds(
|
||
scroll_position,
|
||
line_height,
|
||
gutter_hitbox.bounds,
|
||
hunk,
|
||
snapshot,
|
||
);
|
||
*hitbox = Some(window.insert_hitbox(hunk_bounds, HitboxBehavior::BlockMouse));
|
||
}
|
||
}
|
||
}
|
||
|
||
display_hunks
|
||
}
|
||
|
||
fn layout_inline_diagnostics(
|
||
&self,
|
||
line_layouts: &[LineWithInvisibles],
|
||
crease_trailers: &[Option<CreaseTrailerLayout>],
|
||
row_block_types: &HashMap<DisplayRow, bool>,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
edit_prediction_popover_origin: Option<gpui::Point<Pixels>>,
|
||
start_row: DisplayRow,
|
||
end_row: DisplayRow,
|
||
line_height: Pixels,
|
||
em_width: Pixels,
|
||
style: &EditorStyle,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> HashMap<DisplayRow, AnyElement> {
|
||
let max_severity = match self
|
||
.editor
|
||
.read(cx)
|
||
.inline_diagnostics_enabled()
|
||
.then(|| {
|
||
ProjectSettings::get_global(cx)
|
||
.diagnostics
|
||
.inline
|
||
.max_severity
|
||
.unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity)
|
||
.into_lsp()
|
||
})
|
||
.flatten()
|
||
{
|
||
Some(max_severity) => max_severity,
|
||
None => return HashMap::default(),
|
||
};
|
||
|
||
let active_diagnostics_group = self.editor.read(cx).active_diagnostic_group_id();
|
||
|
||
let diagnostics_by_rows = self.editor.update(cx, |editor, cx| {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
editor
|
||
.inline_diagnostics
|
||
.iter()
|
||
.filter(|(_, diagnostic)| diagnostic.severity <= max_severity)
|
||
.filter(|(_, diagnostic)| match active_diagnostics_group {
|
||
Some(active_diagnostics_group) => {
|
||
// Active diagnostics are all shown in the editor already, no need to display them inline
|
||
diagnostic.group_id != active_diagnostics_group
|
||
}
|
||
None => true,
|
||
})
|
||
.map(|(point, diag)| (point.to_display_point(&snapshot), diag.clone()))
|
||
.skip_while(|(point, _)| point.row() < start_row)
|
||
.take_while(|(point, _)| point.row() < end_row)
|
||
.filter(|(point, _)| !row_block_types.contains_key(&point.row()))
|
||
.fold(HashMap::default(), |mut acc, (point, diagnostic)| {
|
||
acc.entry(point.row())
|
||
.or_insert_with(Vec::new)
|
||
.push(diagnostic);
|
||
acc
|
||
})
|
||
});
|
||
|
||
if diagnostics_by_rows.is_empty() {
|
||
return HashMap::default();
|
||
}
|
||
|
||
let severity_to_color = |sev: &lsp::DiagnosticSeverity| match sev {
|
||
&lsp::DiagnosticSeverity::ERROR => Color::Error,
|
||
&lsp::DiagnosticSeverity::WARNING => Color::Warning,
|
||
&lsp::DiagnosticSeverity::INFORMATION => Color::Info,
|
||
&lsp::DiagnosticSeverity::HINT => Color::Hint,
|
||
_ => Color::Error,
|
||
};
|
||
|
||
let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width;
|
||
let min_x = column_pixels(
|
||
&self.style,
|
||
ProjectSettings::get_global(cx)
|
||
.diagnostics
|
||
.inline
|
||
.min_column as usize,
|
||
window,
|
||
);
|
||
|
||
let mut elements = HashMap::default();
|
||
for (row, mut diagnostics) in diagnostics_by_rows {
|
||
diagnostics.sort_by_key(|diagnostic| {
|
||
(
|
||
diagnostic.severity,
|
||
std::cmp::Reverse(diagnostic.is_primary),
|
||
diagnostic.start.row,
|
||
diagnostic.start.column,
|
||
)
|
||
});
|
||
|
||
let Some(diagnostic_to_render) = diagnostics
|
||
.iter()
|
||
.find(|diagnostic| diagnostic.is_primary)
|
||
.or_else(|| diagnostics.first())
|
||
else {
|
||
continue;
|
||
};
|
||
|
||
let pos_y = content_origin.y + line_height * (row.0 as f64 - scroll_position.y) as f32;
|
||
|
||
let window_ix = row.0.saturating_sub(start_row.0) as usize;
|
||
let pos_x = {
|
||
let crease_trailer_layout = &crease_trailers[window_ix];
|
||
let line_layout = &line_layouts[window_ix];
|
||
|
||
let line_end = if let Some(crease_trailer) = crease_trailer_layout {
|
||
crease_trailer.bounds.right()
|
||
} else {
|
||
Pixels::from(
|
||
ScrollPixelOffset::from(content_origin.x + line_layout.width)
|
||
- scroll_pixel_position.x,
|
||
)
|
||
};
|
||
|
||
let padded_line = line_end + padding;
|
||
let min_start = Pixels::from(
|
||
ScrollPixelOffset::from(content_origin.x + min_x) - scroll_pixel_position.x,
|
||
);
|
||
|
||
cmp::max(padded_line, min_start)
|
||
};
|
||
|
||
let behind_edit_prediction_popover = edit_prediction_popover_origin
|
||
.as_ref()
|
||
.is_some_and(|edit_prediction_popover_origin| {
|
||
(pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y)
|
||
});
|
||
let opacity = if behind_edit_prediction_popover {
|
||
0.5
|
||
} else {
|
||
1.0
|
||
};
|
||
|
||
let mut element = h_flex()
|
||
.id(("diagnostic", row.0))
|
||
.h(line_height)
|
||
.w_full()
|
||
.px_1()
|
||
.rounded_xs()
|
||
.opacity(opacity)
|
||
.bg(severity_to_color(&diagnostic_to_render.severity)
|
||
.color(cx)
|
||
.opacity(0.05))
|
||
.text_color(severity_to_color(&diagnostic_to_render.severity).color(cx))
|
||
.text_sm()
|
||
.font(style.text.font())
|
||
.child(diagnostic_to_render.message.clone())
|
||
.into_any();
|
||
|
||
element.prepaint_as_root(point(pos_x, pos_y), AvailableSpace::min_size(), window, cx);
|
||
|
||
elements.insert(row, element);
|
||
}
|
||
|
||
elements
|
||
}
|
||
|
||
fn layout_inline_code_actions(
|
||
&self,
|
||
display_point: DisplayPoint,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
line_height: Pixels,
|
||
snapshot: &EditorSnapshot,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<AnyElement> {
|
||
// Don't show code actions in split diff view
|
||
if self.split_side.is_some() {
|
||
return None;
|
||
}
|
||
|
||
if !snapshot
|
||
.show_code_actions
|
||
.unwrap_or(EditorSettings::get_global(cx).inline_code_actions)
|
||
{
|
||
return None;
|
||
}
|
||
|
||
let icon_size = ui::IconSize::XSmall;
|
||
let mut button = self.editor.update(cx, |editor, cx| {
|
||
if !editor.has_available_code_actions_for_selection() {
|
||
return None;
|
||
}
|
||
let active = editor
|
||
.context_menu
|
||
.borrow()
|
||
.as_ref()
|
||
.and_then(|menu| {
|
||
if let crate::CodeContextMenu::CodeActions(CodeActionsMenu {
|
||
deployed_from,
|
||
..
|
||
}) = menu
|
||
{
|
||
deployed_from.as_ref()
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.is_some_and(|source| matches!(source, CodeActionSource::Indicator(..)));
|
||
Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx))
|
||
})?;
|
||
|
||
let buffer_point = display_point.to_point(&snapshot.display_snapshot);
|
||
|
||
// do not show code action for folded line
|
||
if snapshot.is_line_folded(MultiBufferRow(buffer_point.row)) {
|
||
return None;
|
||
}
|
||
|
||
// do not show code action for blank line with cursor
|
||
let line_indent = snapshot
|
||
.display_snapshot
|
||
.buffer_snapshot()
|
||
.line_indent_for_row(MultiBufferRow(buffer_point.row));
|
||
if line_indent.is_line_blank() {
|
||
return None;
|
||
}
|
||
|
||
const INLINE_SLOT_CHAR_LIMIT: u32 = 4;
|
||
const MAX_ALTERNATE_DISTANCE: u32 = 8;
|
||
|
||
let is_valid_row = |row_candidate: u32| -> bool {
|
||
// move to other row if folded row
|
||
if snapshot.is_line_folded(MultiBufferRow(row_candidate)) {
|
||
return false;
|
||
}
|
||
if buffer_point.row == row_candidate {
|
||
// move to other row if cursor is in slot
|
||
if buffer_point.column < INLINE_SLOT_CHAR_LIMIT {
|
||
return false;
|
||
}
|
||
} else {
|
||
let candidate_point = MultiBufferPoint {
|
||
row: row_candidate,
|
||
column: 0,
|
||
};
|
||
// move to other row if different excerpt
|
||
let range = if candidate_point < buffer_point {
|
||
candidate_point..buffer_point
|
||
} else {
|
||
buffer_point..candidate_point
|
||
};
|
||
if snapshot
|
||
.display_snapshot
|
||
.buffer_snapshot()
|
||
.excerpt_containing(range)
|
||
.is_none()
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
let line_indent = snapshot
|
||
.display_snapshot
|
||
.buffer_snapshot()
|
||
.line_indent_for_row(MultiBufferRow(row_candidate));
|
||
// use this row if it's blank
|
||
if line_indent.is_line_blank() {
|
||
true
|
||
} else {
|
||
// use this row if code starts after slot
|
||
let indent_size = snapshot
|
||
.display_snapshot
|
||
.buffer_snapshot()
|
||
.indent_size_for_line(MultiBufferRow(row_candidate));
|
||
indent_size.len >= INLINE_SLOT_CHAR_LIMIT
|
||
}
|
||
};
|
||
|
||
let new_buffer_row = if is_valid_row(buffer_point.row) {
|
||
Some(buffer_point.row)
|
||
} else {
|
||
let max_row = snapshot.display_snapshot.buffer_snapshot().max_point().row;
|
||
(1..=MAX_ALTERNATE_DISTANCE).find_map(|offset| {
|
||
let row_above = buffer_point.row.saturating_sub(offset);
|
||
let row_below = buffer_point.row + offset;
|
||
if row_above != buffer_point.row && is_valid_row(row_above) {
|
||
Some(row_above)
|
||
} else if row_below <= max_row && is_valid_row(row_below) {
|
||
Some(row_below)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
}?;
|
||
|
||
let new_display_row = snapshot
|
||
.display_snapshot
|
||
.point_to_display_point(
|
||
Point {
|
||
row: new_buffer_row,
|
||
column: buffer_point.column,
|
||
},
|
||
text::Bias::Left,
|
||
)
|
||
.row();
|
||
|
||
let start_y = content_origin.y
|
||
+ (((new_display_row.as_f64() - scroll_position.y) as f32) * line_height)
|
||
+ (line_height / 2.0)
|
||
- (icon_size.square(window, cx) / 2.);
|
||
let start_x = (ScrollPixelOffset::from(content_origin.x) - scroll_pixel_position.x
|
||
+ ScrollPixelOffset::from(window.rem_size() * 0.1))
|
||
.into();
|
||
|
||
let absolute_offset = gpui::point(start_x, start_y);
|
||
button.layout_as_root(gpui::AvailableSpace::min_size(), window, cx);
|
||
button.prepaint_as_root(
|
||
absolute_offset,
|
||
gpui::AvailableSpace::min_size(),
|
||
window,
|
||
cx,
|
||
);
|
||
Some(button)
|
||
}
|
||
|
||
fn layout_inline_blame(
|
||
&self,
|
||
display_row: DisplayRow,
|
||
row_info: &RowInfo,
|
||
line_layout: &LineWithInvisibles,
|
||
crease_trailer: Option<&CreaseTrailerLayout>,
|
||
em_width: Pixels,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
line_height: Pixels,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<InlineBlameLayout> {
|
||
if !self
|
||
.editor
|
||
.update(cx, |editor, cx| editor.render_git_blame_inline(window, cx))
|
||
{
|
||
return None;
|
||
}
|
||
|
||
let editor = self.editor.read(cx);
|
||
let blame = editor.blame.clone()?;
|
||
let padding = {
|
||
const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.;
|
||
|
||
let mut padding = ProjectSettings::get_global(cx).git.inline_blame.padding as f32;
|
||
|
||
if let Some(edit_prediction) = editor.active_edit_prediction.as_ref()
|
||
&& let EditPrediction::Edit {
|
||
display_mode: EditDisplayMode::TabAccept,
|
||
..
|
||
} = &edit_prediction.completion
|
||
{
|
||
padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS
|
||
}
|
||
|
||
padding * em_width
|
||
};
|
||
|
||
let (buffer_id, entry) = blame
|
||
.update(cx, |blame, cx| {
|
||
blame.blame_for_rows(&[*row_info], cx).next()
|
||
})
|
||
.flatten()?;
|
||
|
||
let mut element = render_inline_blame_entry(entry.clone(), &self.style, cx)?;
|
||
|
||
let start_y =
|
||
content_origin.y + line_height * ((display_row.as_f64() - scroll_position.y) as f32);
|
||
|
||
let start_x = {
|
||
let line_end = if let Some(crease_trailer) = crease_trailer {
|
||
crease_trailer.bounds.right()
|
||
} else {
|
||
Pixels::from(
|
||
ScrollPixelOffset::from(content_origin.x + line_layout.width)
|
||
- scroll_pixel_position.x,
|
||
)
|
||
};
|
||
|
||
let padded_line_end = line_end + padding;
|
||
|
||
let min_column_in_pixels = column_pixels(
|
||
&self.style,
|
||
ProjectSettings::get_global(cx).git.inline_blame.min_column as usize,
|
||
window,
|
||
);
|
||
let min_start = Pixels::from(
|
||
ScrollPixelOffset::from(content_origin.x + min_column_in_pixels)
|
||
- scroll_pixel_position.x,
|
||
);
|
||
|
||
cmp::max(padded_line_end, min_start)
|
||
};
|
||
|
||
let absolute_offset = point(start_x, start_y);
|
||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||
let bounds = Bounds::new(absolute_offset, size);
|
||
|
||
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
|
||
|
||
Some(InlineBlameLayout {
|
||
element,
|
||
bounds,
|
||
buffer_id,
|
||
entry,
|
||
})
|
||
}
|
||
|
||
fn layout_blame_popover(
|
||
&self,
|
||
editor_snapshot: &EditorSnapshot,
|
||
text_hitbox: &Hitbox,
|
||
line_height: Pixels,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
if !self.editor.read(cx).inline_blame_popover.is_some() {
|
||
return;
|
||
}
|
||
|
||
let Some(blame) = self.editor.read(cx).blame.clone() else {
|
||
return;
|
||
};
|
||
let cursor_point = self
|
||
.editor
|
||
.read(cx)
|
||
.selections
|
||
.newest::<language::Point>(&editor_snapshot.display_snapshot)
|
||
.head();
|
||
|
||
let Some((buffer, buffer_point)) = editor_snapshot
|
||
.buffer_snapshot()
|
||
.point_to_buffer_point(cursor_point)
|
||
else {
|
||
return;
|
||
};
|
||
|
||
let row_info = RowInfo {
|
||
buffer_id: Some(buffer.remote_id()),
|
||
buffer_row: Some(buffer_point.row),
|
||
..Default::default()
|
||
};
|
||
|
||
let Some((buffer_id, blame_entry)) = blame
|
||
.update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
|
||
.flatten()
|
||
else {
|
||
return;
|
||
};
|
||
|
||
let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| {
|
||
editor
|
||
.inline_blame_popover
|
||
.as_ref()
|
||
.map(|state| (state.popover_state.clone(), state.position))
|
||
}) else {
|
||
return;
|
||
};
|
||
|
||
let workspace = self
|
||
.editor
|
||
.read_with(cx, |editor, _| editor.workspace().map(|w| w.downgrade()));
|
||
|
||
let maybe_element = workspace.and_then(|workspace| {
|
||
render_blame_entry_popover(
|
||
blame_entry,
|
||
popover_state.scroll_handle,
|
||
popover_state.commit_message,
|
||
popover_state.markdown,
|
||
workspace,
|
||
&blame,
|
||
buffer_id,
|
||
window,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
if let Some(mut element) = maybe_element {
|
||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||
let overall_height = size.height + HOVER_POPOVER_GAP;
|
||
let popover_origin = if target_point.y > overall_height {
|
||
point(target_point.x, target_point.y - size.height)
|
||
} else {
|
||
point(
|
||
target_point.x,
|
||
target_point.y + line_height + HOVER_POPOVER_GAP,
|
||
)
|
||
};
|
||
|
||
let horizontal_offset = (text_hitbox.top_right().x
|
||
- POPOVER_RIGHT_OFFSET
|
||
- (popover_origin.x + size.width))
|
||
.min(Pixels::ZERO);
|
||
|
||
let origin = point(popover_origin.x + horizontal_offset, popover_origin.y);
|
||
let popover_bounds = Bounds::new(origin, size);
|
||
|
||
self.editor.update(cx, |editor, _| {
|
||
if let Some(state) = &mut editor.inline_blame_popover {
|
||
state.popover_bounds = Some(popover_bounds);
|
||
}
|
||
});
|
||
|
||
window.defer_draw(element, origin, 2, None);
|
||
}
|
||
}
|
||
|
||
fn layout_blame_entries(
|
||
&self,
|
||
buffer_rows: &[RowInfo],
|
||
em_width: Pixels,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
start_row: DisplayRow,
|
||
line_height: Pixels,
|
||
gutter_hitbox: &Hitbox,
|
||
max_width: Option<Pixels>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<Vec<AnyElement>> {
|
||
if !self
|
||
.editor
|
||
.update(cx, |editor, cx| editor.render_git_blame_gutter(cx))
|
||
{
|
||
return None;
|
||
}
|
||
|
||
let blame = self.editor.read(cx).blame.clone()?;
|
||
let workspace = self.editor.read(cx).workspace()?;
|
||
let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
|
||
blame.blame_for_rows(buffer_rows, cx).collect()
|
||
});
|
||
|
||
let width = if let Some(max_width) = max_width {
|
||
AvailableSpace::Definite(max_width)
|
||
} else {
|
||
AvailableSpace::MaxContent
|
||
};
|
||
let start_x = em_width;
|
||
|
||
let mut last_used_color: Option<(Hsla, Oid)> = None;
|
||
let blame_renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||
|
||
let shaped_lines = blamed_rows
|
||
.into_iter()
|
||
.enumerate()
|
||
.flat_map(|(ix, blame_entry)| {
|
||
let (buffer_id, blame_entry) = blame_entry?;
|
||
let mut element = render_blame_entry(
|
||
ix,
|
||
&blame,
|
||
blame_entry,
|
||
&self.style,
|
||
&mut last_used_color,
|
||
self.editor.clone(),
|
||
workspace.clone(),
|
||
buffer_id,
|
||
&*blame_renderer,
|
||
window,
|
||
cx,
|
||
)?;
|
||
|
||
let start_y = line_height
|
||
* (DisplayRow(start_row.0 + ix as u32).as_f64() - scroll_position.y) as f32;
|
||
let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
|
||
|
||
element.prepaint_as_root(
|
||
absolute_offset,
|
||
size(width, AvailableSpace::MinContent),
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
Some(element)
|
||
})
|
||
.collect();
|
||
|
||
Some(shaped_lines)
|
||
}
|
||
|
||
fn layout_indent_guides(
|
||
&self,
|
||
content_origin: gpui::Point<Pixels>,
|
||
text_origin: gpui::Point<Pixels>,
|
||
visible_buffer_range: Range<MultiBufferRow>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
line_height: Pixels,
|
||
snapshot: &DisplaySnapshot,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<Vec<IndentGuideLayout>> {
|
||
let indent_guides = self.editor.update(cx, |editor, cx| {
|
||
editor.indent_guides(visible_buffer_range, snapshot, cx)
|
||
})?;
|
||
|
||
let active_indent_guide_indices = self.editor.update(cx, |editor, cx| {
|
||
editor
|
||
.find_active_indent_guide_indices(&indent_guides, snapshot, window, cx)
|
||
.unwrap_or_default()
|
||
});
|
||
|
||
Some(
|
||
indent_guides
|
||
.into_iter()
|
||
.enumerate()
|
||
.filter_map(|(i, indent_guide)| {
|
||
let single_indent_width =
|
||
column_pixels(&self.style, indent_guide.tab_size as usize, window);
|
||
let total_width = single_indent_width * indent_guide.depth as f32;
|
||
let start_x = Pixels::from(
|
||
ScrollOffset::from(content_origin.x + total_width)
|
||
- scroll_pixel_position.x,
|
||
);
|
||
if start_x >= text_origin.x {
|
||
let (offset_y, length, display_row_range) =
|
||
Self::calculate_indent_guide_bounds(
|
||
indent_guide.start_row..indent_guide.end_row,
|
||
line_height,
|
||
snapshot,
|
||
);
|
||
|
||
let start_y = Pixels::from(
|
||
ScrollOffset::from(content_origin.y) + offset_y
|
||
- scroll_pixel_position.y,
|
||
);
|
||
|
||
Some(IndentGuideLayout {
|
||
origin: point(start_x, start_y),
|
||
length,
|
||
single_indent_width,
|
||
display_row_range,
|
||
depth: indent_guide.depth,
|
||
active: active_indent_guide_indices.contains(&i),
|
||
settings: indent_guide.settings,
|
||
})
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.collect(),
|
||
)
|
||
}
|
||
|
||
fn depth_zero_indent_guide_padding_for_row(
|
||
indent_guides: &[IndentGuideLayout],
|
||
row: DisplayRow,
|
||
) -> Pixels {
|
||
indent_guides
|
||
.iter()
|
||
.find(|guide| guide.depth == 0 && guide.display_row_range.contains(&row))
|
||
.and_then(|guide| {
|
||
guide
|
||
.settings
|
||
.visible_line_width(guide.active)
|
||
.map(|width| px(width as f32 * 2.0))
|
||
})
|
||
.unwrap_or(px(0.0))
|
||
}
|
||
|
||
fn layout_wrap_guides(
|
||
&self,
|
||
em_advance: Pixels,
|
||
scroll_position: gpui::Point<f64>,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scrollbar_layout: Option<&EditorScrollbars>,
|
||
vertical_scrollbar_width: Pixels,
|
||
hitbox: &Hitbox,
|
||
window: &Window,
|
||
cx: &App,
|
||
) -> SmallVec<[(Pixels, bool); 2]> {
|
||
let scroll_left = scroll_position.x as f32 * em_advance;
|
||
let content_origin = content_origin.x;
|
||
let horizontal_offset = content_origin - scroll_left;
|
||
let vertical_scrollbar_width = scrollbar_layout
|
||
.and_then(|layout| layout.visible.then_some(vertical_scrollbar_width))
|
||
.unwrap_or_default();
|
||
|
||
self.editor
|
||
.read(cx)
|
||
.wrap_guides(cx)
|
||
.into_iter()
|
||
.flat_map(|(guide, active)| {
|
||
let wrap_position = column_pixels(&self.style, guide, window);
|
||
let wrap_guide_x = wrap_position + horizontal_offset;
|
||
let display_wrap_guide = wrap_guide_x >= content_origin
|
||
&& wrap_guide_x <= hitbox.bounds.right() - vertical_scrollbar_width;
|
||
|
||
display_wrap_guide.then_some((wrap_guide_x, active))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn calculate_indent_guide_bounds(
|
||
row_range: Range<MultiBufferRow>,
|
||
line_height: Pixels,
|
||
snapshot: &DisplaySnapshot,
|
||
) -> (f64, gpui::Pixels, Range<DisplayRow>) {
|
||
let start_point = Point::new(row_range.start.0, 0);
|
||
let end_point = Point::new(row_range.end.0, 0);
|
||
|
||
let mut row_range = start_point.to_display_point(snapshot).row()
|
||
..end_point.to_display_point(snapshot).row();
|
||
|
||
let mut prev_line = start_point;
|
||
prev_line.row = prev_line.row.saturating_sub(1);
|
||
let prev_line = prev_line.to_display_point(snapshot).row();
|
||
|
||
let mut cons_line = end_point;
|
||
cons_line.row += 1;
|
||
let cons_line = cons_line.to_display_point(snapshot).row();
|
||
|
||
let mut offset_y = row_range.start.as_f64() * f64::from(line_height);
|
||
let mut length = (cons_line.0.saturating_sub(row_range.start.0)) as f32 * line_height;
|
||
|
||
// If we are at the end of the buffer, ensure that the indent guide extends to the end of the line.
|
||
if row_range.end == cons_line {
|
||
length += line_height;
|
||
}
|
||
|
||
// If there is a block (e.g. diagnostic) in between the start of the indent guide and the line above,
|
||
// we want to extend the indent guide to the start of the block.
|
||
let mut block_height = 0;
|
||
let mut block_offset = 0;
|
||
let mut found_excerpt_header = false;
|
||
for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
|
||
if matches!(
|
||
block,
|
||
Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
|
||
) {
|
||
found_excerpt_header = true;
|
||
break;
|
||
}
|
||
block_offset += block.height();
|
||
block_height += block.height();
|
||
}
|
||
if !found_excerpt_header {
|
||
offset_y -= block_offset as f64 * f64::from(line_height);
|
||
length += block_height as f32 * line_height;
|
||
row_range = DisplayRow(row_range.start.0.saturating_sub(block_offset))..row_range.end;
|
||
}
|
||
|
||
// If there is a block (e.g. diagnostic) at the end of an multibuffer excerpt,
|
||
// we want to ensure that the indent guide stops before the excerpt header.
|
||
let mut block_height = 0;
|
||
let mut found_excerpt_header = false;
|
||
for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
|
||
if matches!(
|
||
block,
|
||
Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
|
||
) {
|
||
found_excerpt_header = true;
|
||
}
|
||
block_height += block.height();
|
||
}
|
||
if found_excerpt_header {
|
||
length -= block_height as f32 * line_height;
|
||
} else {
|
||
row_range = row_range.start..cons_line;
|
||
}
|
||
|
||
(offset_y, length, row_range)
|
||
}
|
||
|
||
fn layout_bookmarks(
|
||
&self,
|
||
gutter: &Gutter<'_>,
|
||
bookmarks: &HashSet<DisplayRow>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<AnyElement> {
|
||
if self.split_side == Some(SplitSide::Left) {
|
||
return Vec::new();
|
||
}
|
||
|
||
self.editor.update(cx, |editor, cx| {
|
||
bookmarks
|
||
.iter()
|
||
.filter_map(|row| {
|
||
gutter.layout_item_skipping_folds(
|
||
*row,
|
||
|cx, _| editor.render_bookmark(*row, cx).into_any_element(),
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.collect_vec()
|
||
})
|
||
}
|
||
|
||
fn layout_gutter_hover_button(
|
||
&self,
|
||
gutter: &Gutter,
|
||
position: Anchor,
|
||
row: DisplayRow,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<AnyElement> {
|
||
if self.split_side == Some(SplitSide::Left) {
|
||
return None;
|
||
}
|
||
|
||
self.editor.update(cx, |editor, cx| {
|
||
gutter.layout_item_skipping_folds(
|
||
row,
|
||
|cx, window| {
|
||
editor
|
||
.render_gutter_hover_button(position, row, window, cx)
|
||
.into_any_element()
|
||
},
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
}
|
||
|
||
fn layout_breakpoints(
|
||
&self,
|
||
gutter: &Gutter,
|
||
breakpoints: &HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<AnyElement> {
|
||
if self.split_side == Some(SplitSide::Left) {
|
||
return Vec::new();
|
||
}
|
||
|
||
self.editor.update(cx, |editor, cx| {
|
||
breakpoints
|
||
.iter()
|
||
.filter_map(|(row, (text_anchor, bp, state))| {
|
||
gutter.layout_item_skipping_folds(
|
||
*row,
|
||
|cx, _| {
|
||
editor
|
||
.render_breakpoint(*text_anchor, *row, &bp, *state, cx)
|
||
.into_any_element()
|
||
},
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.collect_vec()
|
||
})
|
||
}
|
||
|
||
fn should_render_diff_review_button(
|
||
&self,
|
||
range: Range<DisplayRow>,
|
||
row_infos: &[RowInfo],
|
||
snapshot: &EditorSnapshot,
|
||
cx: &App,
|
||
) -> Option<(DisplayRow, Option<u32>)> {
|
||
if !cx.has_flag::<DiffReviewFeatureFlag>() {
|
||
return None;
|
||
}
|
||
|
||
let show_diff_review_button = self.editor.read(cx).show_diff_review_button();
|
||
if !show_diff_review_button {
|
||
return None;
|
||
}
|
||
|
||
let indicator = self.editor.read(cx).gutter_diff_review_indicator.0?;
|
||
if !indicator.is_active {
|
||
return None;
|
||
}
|
||
|
||
let display_row = indicator
|
||
.start
|
||
.to_display_point(&snapshot.display_snapshot)
|
||
.row();
|
||
let row_index = (display_row.0.saturating_sub(range.start.0)) as usize;
|
||
|
||
let row_info = row_infos.get(row_index);
|
||
if row_info.is_some_and(|row_info| row_info.expand_info.is_some()) {
|
||
return None;
|
||
}
|
||
|
||
let buffer_id = row_info.and_then(|info| info.buffer_id);
|
||
if buffer_id.is_none() {
|
||
return None;
|
||
}
|
||
|
||
let editor = self.editor.read(cx);
|
||
if buffer_id.is_some_and(|buffer_id| editor.is_buffer_folded(buffer_id, cx)) {
|
||
return None;
|
||
}
|
||
|
||
let buffer_row = row_info.and_then(|info| info.buffer_row);
|
||
Some((display_row, buffer_row))
|
||
}
|
||
|
||
fn layout_run_indicators(
|
||
&self,
|
||
gutter: &Gutter,
|
||
run_indicators: &HashSet<DisplayRow>,
|
||
breakpoints: &HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<AnyElement> {
|
||
if self.split_side == Some(SplitSide::Left) {
|
||
return Vec::new();
|
||
}
|
||
|
||
self.editor.update(cx, |editor, cx| {
|
||
let active_task_indicator_row =
|
||
// TODO: add edit button on the right side of each row in the context menu
|
||
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
|
||
deployed_from,
|
||
actions,
|
||
..
|
||
})) = editor.context_menu.borrow().as_ref()
|
||
{
|
||
actions
|
||
.tasks()
|
||
.map(|tasks| tasks.position.to_display_point(gutter.snapshot).row())
|
||
.or_else(|| match deployed_from {
|
||
Some(CodeActionSource::Indicator(row)) => Some(*row),
|
||
_ => None,
|
||
})
|
||
} else {
|
||
None
|
||
};
|
||
|
||
run_indicators
|
||
.iter()
|
||
.filter_map(|display_row| {
|
||
gutter.layout_item(
|
||
*display_row,
|
||
|cx, _| {
|
||
editor
|
||
.render_run_indicator(
|
||
&self.style,
|
||
Some(*display_row) == active_task_indicator_row,
|
||
breakpoints.get(&display_row).map(|(anchor, _, _)| *anchor),
|
||
*display_row,
|
||
cx,
|
||
)
|
||
.into_any_element()
|
||
},
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.collect_vec()
|
||
})
|
||
}
|
||
|
||
fn layout_expand_toggles(
|
||
&self,
|
||
gutter_hitbox: &Hitbox,
|
||
gutter_dimensions: GutterDimensions,
|
||
em_width: Pixels,
|
||
line_height: Pixels,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
start_row: DisplayRow,
|
||
buffer_rows: &[RowInfo],
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<Option<(AnyElement, gpui::Point<Pixels>)>> {
|
||
if self.editor.read(cx).disable_expand_excerpt_buttons {
|
||
return vec![];
|
||
}
|
||
|
||
let editor_font_size = self.style.text.font_size.to_pixels(window.rem_size()) * 1.2;
|
||
|
||
let max_line_number_length = self
|
||
.editor
|
||
.read(cx)
|
||
.buffer()
|
||
.read(cx)
|
||
.snapshot(cx)
|
||
.widest_line_number()
|
||
.ilog10()
|
||
+ 1;
|
||
|
||
let git_gutter_width = Self::gutter_strip_width(line_height)
|
||
+ gutter_dimensions
|
||
.git_blame_entries_width
|
||
.unwrap_or_default();
|
||
let available_width = gutter_dimensions.left_padding - git_gutter_width;
|
||
|
||
buffer_rows
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(ix, row_info)| {
|
||
let ExpandInfo {
|
||
direction,
|
||
start_anchor,
|
||
} = row_info.expand_info?;
|
||
|
||
let icon_name = match direction {
|
||
ExpandExcerptDirection::Up => IconName::ExpandUp,
|
||
ExpandExcerptDirection::Down => IconName::ExpandDown,
|
||
ExpandExcerptDirection::UpAndDown => IconName::ExpandVertical,
|
||
};
|
||
|
||
let editor = self.editor.clone();
|
||
let is_wide = max_line_number_length
|
||
>= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32
|
||
&& row_info
|
||
.buffer_row
|
||
.is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)
|
||
|| gutter_dimensions.right_padding == px(0.);
|
||
|
||
let width = if is_wide {
|
||
available_width - px(5.)
|
||
} else {
|
||
available_width + em_width - px(5.)
|
||
};
|
||
|
||
let toggle = IconButton::new(("expand", ix), icon_name)
|
||
.icon_color(Color::Custom(cx.theme().colors().editor_line_number))
|
||
.icon_size(IconSize::Custom(rems(editor_font_size / window.rem_size())))
|
||
.width(width)
|
||
.on_click(move |_, window, cx| {
|
||
editor.update(cx, |editor, cx| {
|
||
editor.expand_excerpt(start_anchor, direction, window, cx);
|
||
});
|
||
})
|
||
.tooltip(Tooltip::for_action_title(
|
||
"Expand Excerpt",
|
||
&crate::actions::ExpandExcerpts::default(),
|
||
))
|
||
.into_any_element();
|
||
|
||
let position = point(
|
||
git_gutter_width + px(1.),
|
||
line_height
|
||
* (DisplayRow(start_row.0 + ix as u32).as_f64() - scroll_position.y) as f32
|
||
+ px(1.),
|
||
);
|
||
let origin = gutter_hitbox.origin + position;
|
||
|
||
Some((toggle, origin))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn layout_line_numbers(
|
||
&self,
|
||
gutter: &Gutter<'_>,
|
||
active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||
current_selection_head: Option<DisplayRow>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> {
|
||
let include_line_numbers = gutter
|
||
.snapshot
|
||
.show_line_numbers
|
||
.unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
|
||
if !include_line_numbers {
|
||
return Arc::default();
|
||
}
|
||
|
||
let relative = self.editor.read(cx).relative_line_numbers(cx);
|
||
|
||
let relative_line_numbers_enabled = relative.enabled();
|
||
let relative_rows = if relative_line_numbers_enabled
|
||
&& let Some(current_selection_head) = current_selection_head
|
||
{
|
||
gutter.snapshot.calculate_relative_line_numbers(
|
||
&gutter.range,
|
||
current_selection_head,
|
||
relative.wrapped(),
|
||
)
|
||
} else {
|
||
Default::default()
|
||
};
|
||
|
||
let mut line_number = String::new();
|
||
let segments = gutter
|
||
.row_infos
|
||
.iter()
|
||
.enumerate()
|
||
.flat_map(|(ix, row_info)| {
|
||
let display_row = DisplayRow(gutter.range.start.0 + ix as u32);
|
||
line_number.clear();
|
||
let non_relative_number = if relative.wrapped() {
|
||
row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1
|
||
} else {
|
||
row_info.buffer_row? + 1
|
||
};
|
||
let relative_number = relative_rows.get(&display_row);
|
||
if !(relative_line_numbers_enabled && relative_number.is_some())
|
||
&& !gutter.snapshot.number_deleted_lines
|
||
&& row_info
|
||
.diff_status
|
||
.is_some_and(|status| status.is_deleted())
|
||
{
|
||
return None;
|
||
}
|
||
|
||
let number = relative_number.unwrap_or(&non_relative_number);
|
||
write!(&mut line_number, "{number}").unwrap();
|
||
|
||
let color = active_rows
|
||
.get(&display_row)
|
||
.map(|spec| {
|
||
if spec.breakpoint {
|
||
cx.theme().colors().debugger_accent
|
||
} else {
|
||
cx.theme().colors().editor_active_line_number
|
||
}
|
||
})
|
||
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
|
||
let shaped_line =
|
||
self.shape_line_number(SharedString::from(&line_number), color, window);
|
||
let scroll_top =
|
||
gutter.scroll_position.y * ScrollPixelOffset::from(gutter.line_height);
|
||
let line_origin = gutter.hitbox.origin
|
||
+ point(
|
||
gutter.hitbox.size.width
|
||
- shaped_line.width
|
||
- gutter.dimensions.right_padding,
|
||
ix as f32 * gutter.line_height
|
||
- Pixels::from(
|
||
scroll_top % ScrollPixelOffset::from(gutter.line_height),
|
||
),
|
||
);
|
||
|
||
#[cfg(not(test))]
|
||
let hitbox = Some(window.insert_hitbox(
|
||
Bounds::new(line_origin, size(shaped_line.width, gutter.line_height)),
|
||
HitboxBehavior::Normal,
|
||
));
|
||
#[cfg(test)]
|
||
let hitbox = {
|
||
let _ = line_origin;
|
||
None
|
||
};
|
||
|
||
let segment = LineNumberSegment {
|
||
shaped_line,
|
||
hitbox,
|
||
};
|
||
|
||
let buffer_row = DisplayPoint::new(display_row, 0)
|
||
.to_point(gutter.snapshot)
|
||
.row;
|
||
let multi_buffer_row = MultiBufferRow(buffer_row);
|
||
|
||
Some((multi_buffer_row, segment))
|
||
});
|
||
|
||
let mut line_numbers: HashMap<MultiBufferRow, LineNumberLayout> = HashMap::default();
|
||
for (buffer_row, segment) in segments {
|
||
line_numbers
|
||
.entry(buffer_row)
|
||
.or_insert_with(|| LineNumberLayout {
|
||
segments: Default::default(),
|
||
})
|
||
.segments
|
||
.push(segment);
|
||
}
|
||
Arc::new(line_numbers)
|
||
}
|
||
|
||
fn layout_crease_toggles(
|
||
&self,
|
||
rows: Range<DisplayRow>,
|
||
row_infos: &[RowInfo],
|
||
active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||
snapshot: &EditorSnapshot,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<Option<AnyElement>> {
|
||
let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
|
||
&& snapshot.mode.is_full()
|
||
&& self.editor.read(cx).buffer_kind(cx) == ItemBufferKind::Singleton;
|
||
if include_fold_statuses {
|
||
row_infos
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(ix, info)| {
|
||
if info.expand_info.is_some() {
|
||
return None;
|
||
}
|
||
let row = info.multibuffer_row?;
|
||
let display_row = DisplayRow(rows.start.0 + ix as u32);
|
||
let active = active_rows.contains_key(&display_row);
|
||
|
||
snapshot.render_crease_toggle(row, active, self.editor.clone(), window, cx)
|
||
})
|
||
.collect()
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
}
|
||
|
||
fn layout_crease_trailers(
|
||
&self,
|
||
buffer_rows: impl IntoIterator<Item = RowInfo>,
|
||
snapshot: &EditorSnapshot,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<Option<AnyElement>> {
|
||
buffer_rows
|
||
.into_iter()
|
||
.map(|row_info| {
|
||
if row_info.expand_info.is_some() {
|
||
return None;
|
||
}
|
||
if let Some(row) = row_info.multibuffer_row {
|
||
snapshot.render_crease_trailer(row, window, cx)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn bg_segments_per_row(
|
||
rows: Range<DisplayRow>,
|
||
selections: &[(PlayerColor, Vec<SelectionLayout>)],
|
||
highlight_ranges: impl IntoIterator<Item = (Range<DisplayPoint>, Hsla)>,
|
||
base_background: Hsla,
|
||
) -> Vec<Vec<(Range<DisplayPoint>, Hsla)>> {
|
||
if rows.start >= rows.end {
|
||
return Vec::new();
|
||
}
|
||
if !base_background.is_opaque() {
|
||
// We don't actually know what color is behind this editor.
|
||
return Vec::new();
|
||
}
|
||
let highlight_iter = highlight_ranges.into_iter();
|
||
let selection_iter = selections.iter().flat_map(|(player_color, layouts)| {
|
||
let color = player_color.selection;
|
||
layouts.iter().filter_map(move |selection_layout| {
|
||
if selection_layout.range.start != selection_layout.range.end {
|
||
Some((selection_layout.range.clone(), color))
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
});
|
||
let mut per_row_map = vec![Vec::new(); rows.len()];
|
||
for (range, color) in highlight_iter.chain(selection_iter) {
|
||
let covered_rows = if range.end.column() == 0 {
|
||
cmp::max(range.start.row(), rows.start)..cmp::min(range.end.row(), rows.end)
|
||
} else {
|
||
cmp::max(range.start.row(), rows.start)
|
||
..cmp::min(range.end.row().next_row(), rows.end)
|
||
};
|
||
for row in covered_rows.iter_rows() {
|
||
let seg_start = if row == range.start.row() {
|
||
range.start
|
||
} else {
|
||
DisplayPoint::new(row, 0)
|
||
};
|
||
let seg_end = if row == range.end.row() && range.end.column() != 0 {
|
||
range.end
|
||
} else {
|
||
DisplayPoint::new(row, u32::MAX)
|
||
};
|
||
let ix = row.minus(rows.start) as usize;
|
||
debug_assert!(row >= rows.start && row < rows.end);
|
||
debug_assert!(ix < per_row_map.len());
|
||
per_row_map[ix].push((seg_start..seg_end, color));
|
||
}
|
||
}
|
||
for row_segments in per_row_map.iter_mut() {
|
||
if row_segments.is_empty() {
|
||
continue;
|
||
}
|
||
let segments = mem::take(row_segments);
|
||
let merged = Self::merge_overlapping_ranges(segments, base_background);
|
||
*row_segments = merged;
|
||
}
|
||
per_row_map
|
||
}
|
||
|
||
/// Merge overlapping ranges by splitting at all range boundaries and blending colors where
|
||
/// multiple ranges overlap. The result contains non-overlapping ranges ordered from left to right.
|
||
///
|
||
/// Expects `start.row() == end.row()` for each range.
|
||
fn merge_overlapping_ranges(
|
||
ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||
base_background: Hsla,
|
||
) -> Vec<(Range<DisplayPoint>, Hsla)> {
|
||
struct Boundary {
|
||
pos: DisplayPoint,
|
||
is_start: bool,
|
||
index: usize,
|
||
color: Hsla,
|
||
}
|
||
|
||
let mut boundaries: SmallVec<[Boundary; 16]> = SmallVec::with_capacity(ranges.len() * 2);
|
||
for (index, (range, color)) in ranges.iter().enumerate() {
|
||
debug_assert!(
|
||
range.start.row() == range.end.row(),
|
||
"expects single-row ranges"
|
||
);
|
||
if range.start < range.end {
|
||
boundaries.push(Boundary {
|
||
pos: range.start,
|
||
is_start: true,
|
||
index,
|
||
color: *color,
|
||
});
|
||
boundaries.push(Boundary {
|
||
pos: range.end,
|
||
is_start: false,
|
||
index,
|
||
color: *color,
|
||
});
|
||
}
|
||
}
|
||
|
||
if boundaries.is_empty() {
|
||
return Vec::new();
|
||
}
|
||
|
||
boundaries
|
||
.sort_unstable_by(|a, b| a.pos.cmp(&b.pos).then_with(|| a.is_start.cmp(&b.is_start)));
|
||
|
||
let mut processed_ranges: Vec<(Range<DisplayPoint>, Hsla)> = Vec::new();
|
||
let mut active_ranges: SmallVec<[(usize, Hsla); 8]> = SmallVec::new();
|
||
|
||
let mut i = 0;
|
||
let mut start_pos = boundaries[0].pos;
|
||
|
||
let boundaries_len = boundaries.len();
|
||
while i < boundaries_len {
|
||
let current_boundary_pos = boundaries[i].pos;
|
||
if start_pos < current_boundary_pos {
|
||
if !active_ranges.is_empty() {
|
||
let mut color = base_background;
|
||
for &(_, c) in &active_ranges {
|
||
color = Hsla::blend(color, c);
|
||
}
|
||
if let Some((last_range, last_color)) = processed_ranges.last_mut() {
|
||
if *last_color == color && last_range.end == start_pos {
|
||
last_range.end = current_boundary_pos;
|
||
} else {
|
||
processed_ranges.push((start_pos..current_boundary_pos, color));
|
||
}
|
||
} else {
|
||
processed_ranges.push((start_pos..current_boundary_pos, color));
|
||
}
|
||
}
|
||
}
|
||
while i < boundaries_len && boundaries[i].pos == current_boundary_pos {
|
||
let active_range = &boundaries[i];
|
||
if active_range.is_start {
|
||
let idx = active_range.index;
|
||
let pos = active_ranges
|
||
.binary_search_by_key(&idx, |(i, _)| *i)
|
||
.unwrap_or_else(|p| p);
|
||
active_ranges.insert(pos, (idx, active_range.color));
|
||
} else {
|
||
let idx = active_range.index;
|
||
if let Ok(pos) = active_ranges.binary_search_by_key(&idx, |(i, _)| *i) {
|
||
active_ranges.remove(pos);
|
||
}
|
||
}
|
||
i += 1;
|
||
}
|
||
start_pos = current_boundary_pos;
|
||
}
|
||
|
||
processed_ranges
|
||
}
|
||
|
||
fn layout_lines(
|
||
rows: Range<DisplayRow>,
|
||
snapshot: &EditorSnapshot,
|
||
style: &EditorStyle,
|
||
editor_width: Pixels,
|
||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||
bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<LineWithInvisibles> {
|
||
if rows.start >= rows.end {
|
||
return Vec::new();
|
||
}
|
||
|
||
// Show the placeholder when the editor is empty
|
||
if snapshot.is_empty() {
|
||
let font_size = style.text.font_size.to_pixels(window.rem_size());
|
||
let placeholder_color = cx.theme().colors().text_placeholder;
|
||
let placeholder_text = snapshot.placeholder_text();
|
||
|
||
let placeholder_lines = placeholder_text
|
||
.as_ref()
|
||
.map_or(Vec::new(), |text| text.split('\n').collect::<Vec<_>>());
|
||
|
||
let placeholder_line_count = placeholder_lines.len();
|
||
|
||
placeholder_lines
|
||
.into_iter()
|
||
.skip(rows.start.0 as usize)
|
||
.chain(iter::repeat(""))
|
||
.take(cmp::max(rows.len(), placeholder_line_count))
|
||
.map(move |line| {
|
||
let run = TextRun {
|
||
len: line.len(),
|
||
font: style.text.font(),
|
||
color: placeholder_color,
|
||
..Default::default()
|
||
};
|
||
let line = window.text_system().shape_line(
|
||
line.to_string().into(),
|
||
font_size,
|
||
&[run],
|
||
None,
|
||
);
|
||
LineWithInvisibles {
|
||
width: line.width,
|
||
len: line.len,
|
||
fragments: smallvec![LineFragment::Text(line)],
|
||
invisibles: Vec::new(),
|
||
font_size,
|
||
}
|
||
})
|
||
.collect()
|
||
} else {
|
||
let use_tree_sitter = !snapshot.semantic_tokens_enabled
|
||
|| snapshot.use_tree_sitter_for_syntax(rows.start, cx);
|
||
let language_aware = LanguageAwareStyling {
|
||
tree_sitter: use_tree_sitter,
|
||
diagnostics: true,
|
||
};
|
||
let chunks = snapshot.highlighted_chunks(rows.clone(), language_aware, style);
|
||
LineWithInvisibles::from_chunks(
|
||
chunks,
|
||
style,
|
||
MAX_LINE_LEN,
|
||
rows.len(),
|
||
&snapshot.mode,
|
||
editor_width,
|
||
is_row_soft_wrapped,
|
||
bg_segments_per_row,
|
||
window,
|
||
cx,
|
||
)
|
||
}
|
||
}
|
||
|
||
fn prepaint_lines(
|
||
&self,
|
||
start_row: DisplayRow,
|
||
line_layouts: &mut [LineWithInvisibles],
|
||
line_height: Pixels,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
content_origin: gpui::Point<Pixels>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> SmallVec<[AnyElement; 1]> {
|
||
let mut line_elements = SmallVec::new();
|
||
for (ix, line) in line_layouts.iter_mut().enumerate() {
|
||
let row = start_row + DisplayRow(ix as u32);
|
||
line.prepaint(
|
||
line_height,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
row,
|
||
content_origin,
|
||
&mut line_elements,
|
||
window,
|
||
cx,
|
||
);
|
||
}
|
||
line_elements
|
||
}
|
||
|
||
fn render_block(
|
||
&self,
|
||
block: &Block,
|
||
available_width: AvailableSpace,
|
||
block_id: BlockId,
|
||
block_row_start: DisplayRow,
|
||
snapshot: &EditorSnapshot,
|
||
text_x: Pixels,
|
||
rows: &Range<DisplayRow>,
|
||
line_layouts: &[LineWithInvisibles],
|
||
editor_margins: &EditorMargins,
|
||
line_height: Pixels,
|
||
em_width: Pixels,
|
||
text_hitbox: &Hitbox,
|
||
editor_width: Pixels,
|
||
scroll_width: &mut Pixels,
|
||
resized_blocks: &mut HashMap<CustomBlockId, u32>,
|
||
row_block_types: &mut HashMap<DisplayRow, bool>,
|
||
selections: &[Selection<Point>],
|
||
selected_buffer_ids: &Vec<BufferId>,
|
||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||
sticky_header_excerpt_id: Option<BufferId>,
|
||
indent_guides: &Option<Vec<IndentGuideLayout>>,
|
||
block_resize_offset: &mut i32,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<(AnyElement, Size<Pixels>, DisplayRow, Pixels)> {
|
||
let mut x_position = None;
|
||
let mut element = match block {
|
||
Block::Custom(custom) => {
|
||
let block_start = custom.start().to_point(&snapshot.buffer_snapshot());
|
||
let block_end = custom.end().to_point(&snapshot.buffer_snapshot());
|
||
if block.place_near() && snapshot.is_line_folded(MultiBufferRow(block_start.row)) {
|
||
return None;
|
||
}
|
||
let align_to = block_start.to_display_point(snapshot);
|
||
let x_and_width = |layout: &LineWithInvisibles| {
|
||
Some((
|
||
text_x + layout.x_for_index(align_to.column() as usize),
|
||
text_x + layout.width,
|
||
))
|
||
};
|
||
let line_ix = align_to.row().0.checked_sub(rows.start.0);
|
||
x_position =
|
||
if let Some(layout) = line_ix.and_then(|ix| line_layouts.get(ix as usize)) {
|
||
x_and_width(layout)
|
||
} else {
|
||
x_and_width(&layout_line(
|
||
align_to.row(),
|
||
snapshot,
|
||
&self.style,
|
||
editor_width,
|
||
is_row_soft_wrapped,
|
||
window,
|
||
cx,
|
||
))
|
||
};
|
||
|
||
let anchor_x = x_position.unwrap().0;
|
||
|
||
let selected = selections
|
||
.binary_search_by(|selection| {
|
||
if selection.end <= block_start {
|
||
Ordering::Less
|
||
} else if selection.start >= block_end {
|
||
Ordering::Greater
|
||
} else {
|
||
Ordering::Equal
|
||
}
|
||
})
|
||
.is_ok();
|
||
|
||
div()
|
||
.size_full()
|
||
.child(
|
||
custom.render(&mut BlockContext {
|
||
window,
|
||
app: cx,
|
||
anchor_x,
|
||
margins: editor_margins,
|
||
line_height,
|
||
em_width,
|
||
block_id,
|
||
height: custom.height.unwrap_or(1),
|
||
selected,
|
||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||
editor_style: &self.style,
|
||
indent_guide_padding: indent_guides
|
||
.as_ref()
|
||
.map(|guides| {
|
||
Self::depth_zero_indent_guide_padding_for_row(
|
||
guides,
|
||
block_row_start,
|
||
)
|
||
})
|
||
.unwrap_or(px(0.0)),
|
||
}),
|
||
)
|
||
.into_any()
|
||
}
|
||
|
||
Block::FoldedBuffer {
|
||
first_excerpt,
|
||
height,
|
||
..
|
||
} => {
|
||
let mut result = v_flex().id(block_id).w_full().pr(editor_margins.right);
|
||
|
||
if self.should_show_buffer_headers() {
|
||
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id());
|
||
let jump_data = header::header_jump_data(
|
||
snapshot,
|
||
block_row_start,
|
||
*height,
|
||
first_excerpt,
|
||
latest_selection_anchors,
|
||
);
|
||
result = result.child(header::render_buffer_header(
|
||
&self.editor,
|
||
first_excerpt,
|
||
true,
|
||
selected,
|
||
false,
|
||
jump_data,
|
||
window,
|
||
cx,
|
||
));
|
||
} else {
|
||
result =
|
||
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||
}
|
||
|
||
result.into_any_element()
|
||
}
|
||
|
||
Block::ExcerptBoundary { .. } => {
|
||
let color = cx.theme().colors().clone();
|
||
let mut result = v_flex().id(block_id).w_full();
|
||
|
||
result = result.child(
|
||
h_flex().relative().child(
|
||
div()
|
||
.top(line_height / 2.)
|
||
.absolute()
|
||
.w_full()
|
||
.h_px()
|
||
.bg(color.border_variant),
|
||
),
|
||
);
|
||
|
||
result.into_any()
|
||
}
|
||
|
||
Block::BufferHeader { excerpt, height } => {
|
||
let mut result = v_flex().id(block_id).w_full();
|
||
|
||
if self.should_show_buffer_headers() {
|
||
let jump_data = header::header_jump_data(
|
||
snapshot,
|
||
block_row_start,
|
||
*height,
|
||
excerpt,
|
||
latest_selection_anchors,
|
||
);
|
||
|
||
if sticky_header_excerpt_id != Some(excerpt.buffer_id()) {
|
||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id());
|
||
|
||
result = result.child(div().pr(editor_margins.right).child(
|
||
header::render_buffer_header(
|
||
&self.editor,
|
||
excerpt,
|
||
false,
|
||
selected,
|
||
false,
|
||
jump_data,
|
||
window,
|
||
cx,
|
||
),
|
||
));
|
||
} else {
|
||
result =
|
||
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||
}
|
||
} else {
|
||
result =
|
||
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||
}
|
||
|
||
result.into_any()
|
||
}
|
||
|
||
Block::Spacer { height, .. } => {
|
||
let indent_guide_padding = indent_guides
|
||
.as_ref()
|
||
.map(|guides| {
|
||
Self::depth_zero_indent_guide_padding_for_row(guides, block_row_start)
|
||
})
|
||
.unwrap_or(px(0.0));
|
||
Self::render_spacer_block(
|
||
block_id,
|
||
*height,
|
||
line_height,
|
||
indent_guide_padding,
|
||
window,
|
||
cx,
|
||
)
|
||
}
|
||
};
|
||
|
||
// Discover the element's content height, then round up to the nearest multiple of line height.
|
||
let preliminary_size = element.layout_as_root(
|
||
size(available_width, AvailableSpace::MinContent),
|
||
window,
|
||
cx,
|
||
);
|
||
let quantized_height = (preliminary_size.height / line_height).ceil() * line_height;
|
||
let final_size = if preliminary_size.height == quantized_height {
|
||
preliminary_size
|
||
} else {
|
||
element.layout_as_root(size(available_width, quantized_height.into()), window, cx)
|
||
};
|
||
let mut element_height_in_lines = ((final_size.height / line_height).ceil() as u32).max(1);
|
||
|
||
let effective_row_start = block_row_start.0 as i32 + *block_resize_offset;
|
||
debug_assert!(effective_row_start >= 0);
|
||
let mut row = DisplayRow(effective_row_start.max(0) as u32);
|
||
|
||
let mut x_offset = px(0.);
|
||
let mut is_block = true;
|
||
|
||
if let BlockId::Custom(custom_block_id) = block_id
|
||
&& block.has_height()
|
||
{
|
||
if block.place_near()
|
||
&& let Some((x_target, line_width)) = x_position
|
||
{
|
||
let margin = em_width * 2;
|
||
if line_width + final_size.width + margin
|
||
< editor_width + editor_margins.gutter.full_width()
|
||
&& !row_block_types.contains_key(&(row - 1))
|
||
&& element_height_in_lines == 1
|
||
{
|
||
// Render inline at end of line (for diagnostic blocks that fit)
|
||
x_offset = line_width + margin;
|
||
row = row - 1;
|
||
is_block = false;
|
||
element_height_in_lines = 0;
|
||
row_block_types.insert(row, is_block);
|
||
} else {
|
||
let max_offset =
|
||
editor_width + editor_margins.gutter.full_width() - final_size.width;
|
||
let min_offset = (x_target + em_width - final_size.width)
|
||
.max(editor_margins.gutter.full_width());
|
||
x_offset = x_target.min(max_offset).max(min_offset);
|
||
}
|
||
};
|
||
if element_height_in_lines != block.height() {
|
||
*block_resize_offset += element_height_in_lines as i32 - block.height() as i32;
|
||
resized_blocks.insert(custom_block_id, element_height_in_lines);
|
||
}
|
||
}
|
||
for i in 0..element_height_in_lines {
|
||
row_block_types.insert(row + i, is_block);
|
||
}
|
||
|
||
Some((element, final_size, row, x_offset))
|
||
}
|
||
|
||
/// The spacer pattern period must be an even factor of the line height, so
|
||
/// that two consecutive spacer blocks can render contiguously without an
|
||
/// obvious break in the pattern.
|
||
///
|
||
/// Two consecutive spacers can appear when the other side has a diff hunk
|
||
/// and a custom block next to each other (e.g. merge conflict buttons).
|
||
fn spacer_pattern_period(line_height: f32, target_height: f32) -> f32 {
|
||
let k_approx = line_height / (2.0 * target_height);
|
||
let k_floor = (k_approx.floor() as u32).max(1);
|
||
let k_ceil = (k_approx.ceil() as u32).max(1);
|
||
|
||
let size_floor = line_height / (2 * k_floor) as f32;
|
||
let size_ceil = line_height / (2 * k_ceil) as f32;
|
||
|
||
if (size_floor - target_height).abs() <= (size_ceil - target_height).abs() {
|
||
size_floor
|
||
} else {
|
||
size_ceil
|
||
}
|
||
}
|
||
|
||
pub fn render_spacer_block(
|
||
block_id: BlockId,
|
||
block_height: u32,
|
||
line_height: Pixels,
|
||
indent_guide_padding: Pixels,
|
||
window: &mut Window,
|
||
cx: &App,
|
||
) -> AnyElement {
|
||
let target_size = 16.0;
|
||
let scale = window.scale_factor();
|
||
let pattern_size =
|
||
Self::spacer_pattern_period(f32::from(line_height) * scale, target_size * scale);
|
||
let color = cx.theme().colors().panel_background;
|
||
let background = pattern_slash(color, 2.0, pattern_size - 2.0);
|
||
|
||
div()
|
||
.id(block_id)
|
||
.cursor(CursorStyle::Arrow)
|
||
.w_full()
|
||
.h((block_height as f32) * line_height)
|
||
.flex()
|
||
.flex_row()
|
||
.child(div().flex_shrink_0().w(indent_guide_padding).h_full())
|
||
.child(
|
||
div()
|
||
.flex_1()
|
||
.h_full()
|
||
.relative()
|
||
.overflow_x_hidden()
|
||
.child(
|
||
div()
|
||
.absolute()
|
||
.top_0()
|
||
.bottom_0()
|
||
.right_0()
|
||
.left(-indent_guide_padding)
|
||
.bg(background),
|
||
),
|
||
)
|
||
.into_any()
|
||
}
|
||
|
||
fn render_blocks(
|
||
&self,
|
||
rows: Range<DisplayRow>,
|
||
snapshot: &EditorSnapshot,
|
||
hitbox: &Hitbox,
|
||
text_hitbox: &Hitbox,
|
||
editor_width: Pixels,
|
||
scroll_width: &mut Pixels,
|
||
editor_margins: &EditorMargins,
|
||
em_width: Pixels,
|
||
text_x: Pixels,
|
||
line_height: Pixels,
|
||
line_layouts: &mut [LineWithInvisibles],
|
||
selections: &[Selection<Point>],
|
||
selected_buffer_ids: &Vec<BufferId>,
|
||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||
sticky_header_excerpt_id: Option<BufferId>,
|
||
indent_guides: &Option<Vec<IndentGuideLayout>>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> RenderBlocksOutput {
|
||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||
.blocks_in_range(rows.clone())
|
||
.partition::<Vec<_>, _>(|(_, block)| block.style() == BlockStyle::Fixed);
|
||
|
||
let mut focused_block = self
|
||
.editor
|
||
.update(cx, |editor, _| editor.take_focused_block());
|
||
let mut fixed_block_max_width = Pixels::ZERO;
|
||
let mut blocks = Vec::new();
|
||
let mut spacer_blocks = Vec::new();
|
||
let mut resized_blocks = HashMap::default();
|
||
let mut row_block_types = HashMap::default();
|
||
let mut block_resize_offset: i32 = 0;
|
||
|
||
for (row, block) in fixed_blocks {
|
||
let block_id = block.id();
|
||
|
||
if focused_block.as_ref().is_some_and(|b| b.id == block_id) {
|
||
focused_block = None;
|
||
}
|
||
|
||
if let Some((element, element_size, row, x_offset)) = self.render_block(
|
||
block,
|
||
AvailableSpace::MinContent,
|
||
block_id,
|
||
row,
|
||
snapshot,
|
||
text_x,
|
||
&rows,
|
||
line_layouts,
|
||
editor_margins,
|
||
line_height,
|
||
em_width,
|
||
text_hitbox,
|
||
editor_width,
|
||
scroll_width,
|
||
&mut resized_blocks,
|
||
&mut row_block_types,
|
||
selections,
|
||
selected_buffer_ids,
|
||
latest_selection_anchors,
|
||
is_row_soft_wrapped,
|
||
sticky_header_excerpt_id,
|
||
indent_guides,
|
||
&mut block_resize_offset,
|
||
window,
|
||
cx,
|
||
) {
|
||
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
|
||
blocks.push(BlockLayout {
|
||
id: block_id,
|
||
x_offset,
|
||
row: Some(row),
|
||
element,
|
||
available_space: size(AvailableSpace::MinContent, element_size.height.into()),
|
||
style: BlockStyle::Fixed,
|
||
overlaps_gutter: true,
|
||
is_buffer_header: block.is_buffer_header(),
|
||
});
|
||
}
|
||
}
|
||
|
||
for (row, block) in non_fixed_blocks {
|
||
let style = block.style();
|
||
let width = match (style, block.place_near()) {
|
||
(_, true) => AvailableSpace::MinContent,
|
||
(BlockStyle::Sticky, _) => hitbox.size.width.into(),
|
||
(BlockStyle::Flex, _) => hitbox
|
||
.size
|
||
.width
|
||
.max(fixed_block_max_width)
|
||
.max(
|
||
editor_margins.gutter.width + *scroll_width + editor_margins.extended_right,
|
||
)
|
||
.into(),
|
||
(BlockStyle::Spacer, _) => hitbox
|
||
.size
|
||
.width
|
||
.max(fixed_block_max_width)
|
||
.max(*scroll_width + editor_margins.extended_right)
|
||
.into(),
|
||
(BlockStyle::Fixed, _) => unreachable!(),
|
||
};
|
||
let block_id = block.id();
|
||
|
||
if focused_block.as_ref().is_some_and(|b| b.id == block_id) {
|
||
focused_block = None;
|
||
}
|
||
|
||
if let Some((element, element_size, row, x_offset)) = self.render_block(
|
||
block,
|
||
width,
|
||
block_id,
|
||
row,
|
||
snapshot,
|
||
text_x,
|
||
&rows,
|
||
line_layouts,
|
||
editor_margins,
|
||
line_height,
|
||
em_width,
|
||
text_hitbox,
|
||
editor_width,
|
||
scroll_width,
|
||
&mut resized_blocks,
|
||
&mut row_block_types,
|
||
selections,
|
||
selected_buffer_ids,
|
||
latest_selection_anchors,
|
||
is_row_soft_wrapped,
|
||
sticky_header_excerpt_id,
|
||
indent_guides,
|
||
&mut block_resize_offset,
|
||
window,
|
||
cx,
|
||
) {
|
||
let layout = BlockLayout {
|
||
id: block_id,
|
||
x_offset,
|
||
row: Some(row),
|
||
element,
|
||
available_space: size(width, element_size.height.into()),
|
||
style,
|
||
overlaps_gutter: !block.place_near() && style != BlockStyle::Spacer,
|
||
is_buffer_header: block.is_buffer_header(),
|
||
};
|
||
if style == BlockStyle::Spacer {
|
||
spacer_blocks.push(layout);
|
||
} else {
|
||
blocks.push(layout);
|
||
}
|
||
}
|
||
}
|
||
|
||
if let Some(focused_block) = focused_block
|
||
&& let Some(focus_handle) = focused_block.focus_handle.upgrade()
|
||
&& focus_handle.is_focused(window)
|
||
&& let Some(block) = snapshot.block_for_id(focused_block.id)
|
||
{
|
||
let style = block.style();
|
||
let width = match style {
|
||
BlockStyle::Fixed => AvailableSpace::MinContent,
|
||
BlockStyle::Flex => {
|
||
AvailableSpace::Definite(hitbox.size.width.max(fixed_block_max_width).max(
|
||
editor_margins.gutter.width + *scroll_width + editor_margins.extended_right,
|
||
))
|
||
}
|
||
BlockStyle::Spacer => AvailableSpace::Definite(
|
||
hitbox
|
||
.size
|
||
.width
|
||
.max(fixed_block_max_width)
|
||
.max(*scroll_width + editor_margins.extended_right),
|
||
),
|
||
BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
|
||
};
|
||
|
||
if let Some((element, element_size, _, x_offset)) = self.render_block(
|
||
&block,
|
||
width,
|
||
focused_block.id,
|
||
rows.end,
|
||
snapshot,
|
||
text_x,
|
||
&rows,
|
||
line_layouts,
|
||
editor_margins,
|
||
line_height,
|
||
em_width,
|
||
text_hitbox,
|
||
editor_width,
|
||
scroll_width,
|
||
&mut resized_blocks,
|
||
&mut row_block_types,
|
||
selections,
|
||
selected_buffer_ids,
|
||
latest_selection_anchors,
|
||
is_row_soft_wrapped,
|
||
sticky_header_excerpt_id,
|
||
indent_guides,
|
||
&mut block_resize_offset,
|
||
window,
|
||
cx,
|
||
) {
|
||
blocks.push(BlockLayout {
|
||
id: block.id(),
|
||
x_offset,
|
||
row: None,
|
||
element,
|
||
available_space: size(width, element_size.height.into()),
|
||
style,
|
||
overlaps_gutter: true,
|
||
is_buffer_header: block.is_buffer_header(),
|
||
});
|
||
}
|
||
}
|
||
|
||
if resized_blocks.is_empty() {
|
||
*scroll_width =
|
||
(*scroll_width).max(fixed_block_max_width - editor_margins.gutter.width);
|
||
}
|
||
|
||
RenderBlocksOutput {
|
||
non_spacer_blocks: blocks,
|
||
spacer_blocks,
|
||
row_block_types,
|
||
resized_blocks: (!resized_blocks.is_empty()).then_some(resized_blocks),
|
||
}
|
||
}
|
||
|
||
fn layout_blocks(
|
||
&self,
|
||
blocks: &mut Vec<BlockLayout>,
|
||
hitbox: &Hitbox,
|
||
gutter_hitbox: &Hitbox,
|
||
line_height: Pixels,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
editor_margins: &EditorMargins,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for block in blocks {
|
||
let mut origin = if let Some(row) = block.row {
|
||
hitbox.origin
|
||
+ point(
|
||
block.x_offset,
|
||
Pixels::from(
|
||
(row.as_f64() - scroll_position.y)
|
||
* ScrollPixelOffset::from(line_height),
|
||
),
|
||
)
|
||
} else {
|
||
// Position the block outside the visible area
|
||
hitbox.origin + point(Pixels::ZERO, hitbox.size.height)
|
||
};
|
||
|
||
if block.style == BlockStyle::Spacer {
|
||
origin += point(
|
||
gutter_hitbox.size.width + editor_margins.gutter.margin,
|
||
Pixels::ZERO,
|
||
);
|
||
}
|
||
|
||
if !matches!(block.style, BlockStyle::Sticky) {
|
||
origin += point(Pixels::from(-scroll_pixel_position.x), Pixels::ZERO);
|
||
}
|
||
|
||
let focus_handle =
|
||
block
|
||
.element
|
||
.prepaint_as_root(origin, block.available_space, window, cx);
|
||
|
||
if let Some(focus_handle) = focus_handle {
|
||
self.editor.update(cx, |editor, _cx| {
|
||
editor.set_focused_block(FocusedBlock {
|
||
id: block.id,
|
||
focus_handle: focus_handle.downgrade(),
|
||
});
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
fn layout_cursor_popovers(
|
||
&self,
|
||
line_height: Pixels,
|
||
text_hitbox: &Hitbox,
|
||
content_origin: gpui::Point<Pixels>,
|
||
right_margin: Pixels,
|
||
start_row: DisplayRow,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
line_layouts: &[LineWithInvisibles],
|
||
cursor: DisplayPoint,
|
||
cursor_point: Point,
|
||
style: &EditorStyle,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<ContextMenuLayout> {
|
||
let mut min_menu_height = Pixels::ZERO;
|
||
let mut max_menu_height = Pixels::ZERO;
|
||
let mut height_above_menu = Pixels::ZERO;
|
||
let height_below_menu = Pixels::ZERO;
|
||
let mut edit_prediction_popover_visible = false;
|
||
let mut context_menu_visible = false;
|
||
let context_menu_placement;
|
||
|
||
{
|
||
let editor = self.editor.read(cx);
|
||
if editor.edit_prediction_visible_in_cursor_popover(editor.has_active_edit_prediction())
|
||
{
|
||
height_above_menu +=
|
||
editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING;
|
||
edit_prediction_popover_visible = true;
|
||
}
|
||
|
||
if editor.context_menu_visible()
|
||
&& let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin()
|
||
{
|
||
let (min_height_in_lines, max_height_in_lines) = editor
|
||
.context_menu_options
|
||
.as_ref()
|
||
.map_or((3, 12), |options| {
|
||
(options.min_entries_visible, options.max_entries_visible)
|
||
});
|
||
|
||
min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING;
|
||
max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING;
|
||
context_menu_visible = true;
|
||
}
|
||
context_menu_placement = editor
|
||
.context_menu_options
|
||
.as_ref()
|
||
.and_then(|options| options.placement.clone());
|
||
}
|
||
|
||
let visible = edit_prediction_popover_visible || context_menu_visible;
|
||
if !visible {
|
||
return None;
|
||
}
|
||
|
||
let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
|
||
let target_position = content_origin
|
||
+ gpui::Point {
|
||
x: cmp::max(
|
||
px(0.),
|
||
Pixels::from(
|
||
ScrollPixelOffset::from(
|
||
cursor_row_layout.x_for_index(cursor.column() as usize),
|
||
) - scroll_pixel_position.x,
|
||
),
|
||
),
|
||
y: cmp::max(
|
||
px(0.),
|
||
Pixels::from(
|
||
cursor.row().next_row().as_f64() * ScrollPixelOffset::from(line_height)
|
||
- scroll_pixel_position.y,
|
||
),
|
||
),
|
||
};
|
||
|
||
let viewport_bounds =
|
||
Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
|
||
right: -right_margin - MENU_GAP,
|
||
..Default::default()
|
||
});
|
||
|
||
let min_height = height_above_menu + min_menu_height + height_below_menu;
|
||
let max_height = height_above_menu + max_menu_height + height_below_menu;
|
||
let (laid_out_popovers, y_flipped) = self.layout_popovers_above_or_below_line(
|
||
target_position,
|
||
line_height,
|
||
min_height,
|
||
max_height,
|
||
context_menu_placement,
|
||
text_hitbox,
|
||
viewport_bounds,
|
||
window,
|
||
cx,
|
||
|height, max_width_for_stable_x, y_flipped, window, cx| {
|
||
// First layout the menu to get its size - others can be at least this wide.
|
||
let context_menu = if context_menu_visible {
|
||
let menu_height = if y_flipped {
|
||
height - height_below_menu
|
||
} else {
|
||
height - height_above_menu
|
||
};
|
||
let mut element = self
|
||
.render_context_menu(line_height, menu_height, window, cx)
|
||
.expect("Visible context menu should always render.");
|
||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||
Some((CursorPopoverType::CodeContextMenu, element, size))
|
||
} else {
|
||
None
|
||
};
|
||
let min_width = context_menu
|
||
.as_ref()
|
||
.map_or(px(0.), |(_, _, size)| size.width);
|
||
let max_width = max_width_for_stable_x.max(
|
||
context_menu
|
||
.as_ref()
|
||
.map_or(px(0.), |(_, _, size)| size.width),
|
||
);
|
||
|
||
let edit_prediction = if edit_prediction_popover_visible {
|
||
self.editor.update(cx, move |editor, cx| {
|
||
let mut element = editor.render_edit_prediction_cursor_popover(
|
||
min_width,
|
||
max_width,
|
||
cursor_point,
|
||
style,
|
||
window,
|
||
cx,
|
||
)?;
|
||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||
Some((CursorPopoverType::EditPrediction, element, size))
|
||
})
|
||
} else {
|
||
None
|
||
};
|
||
vec![edit_prediction, context_menu]
|
||
.into_iter()
|
||
.flatten()
|
||
.collect::<Vec<_>>()
|
||
},
|
||
)?;
|
||
|
||
let (menu_ix, (_, menu_bounds)) = laid_out_popovers
|
||
.iter()
|
||
.find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))?;
|
||
let last_ix = laid_out_popovers.len() - 1;
|
||
let menu_is_last = menu_ix == last_ix;
|
||
let first_popover_bounds = laid_out_popovers[0].1;
|
||
let last_popover_bounds = laid_out_popovers[last_ix].1;
|
||
|
||
// Bounds to layout the aside around. When y_flipped, the aside goes either above or to the
|
||
// right, and otherwise it goes below or to the right.
|
||
let mut target_bounds = Bounds::from_corners(
|
||
first_popover_bounds.origin,
|
||
last_popover_bounds.bottom_right(),
|
||
);
|
||
target_bounds.size.width = menu_bounds.size.width;
|
||
|
||
// Like `target_bounds`, but with the max height it could occupy. Choosing an aside position
|
||
// based on this is preferred for layout stability.
|
||
let mut max_target_bounds = target_bounds;
|
||
max_target_bounds.size.height = max_height;
|
||
if y_flipped {
|
||
max_target_bounds.origin.y -= max_height - target_bounds.size.height;
|
||
}
|
||
|
||
// Add spacing around `target_bounds` and `max_target_bounds`.
|
||
let mut extend_amount = Edges::all(MENU_GAP);
|
||
if y_flipped {
|
||
extend_amount.bottom = line_height;
|
||
} else {
|
||
extend_amount.top = line_height;
|
||
}
|
||
let target_bounds = target_bounds.extend(extend_amount);
|
||
let max_target_bounds = max_target_bounds.extend(extend_amount);
|
||
|
||
let must_place_above_or_below =
|
||
if y_flipped && !menu_is_last && menu_bounds.size.height < max_menu_height {
|
||
laid_out_popovers[menu_ix + 1..]
|
||
.iter()
|
||
.any(|(_, popover_bounds)| popover_bounds.size.width > menu_bounds.size.width)
|
||
} else {
|
||
false
|
||
};
|
||
|
||
let aside_bounds = self.layout_context_menu_aside(
|
||
y_flipped,
|
||
*menu_bounds,
|
||
target_bounds,
|
||
max_target_bounds,
|
||
max_menu_height,
|
||
must_place_above_or_below,
|
||
text_hitbox,
|
||
viewport_bounds,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
if let Some(menu_bounds) = laid_out_popovers.iter().find_map(|(popover_type, bounds)| {
|
||
if matches!(popover_type, CursorPopoverType::CodeContextMenu) {
|
||
Some(*bounds)
|
||
} else {
|
||
None
|
||
}
|
||
}) {
|
||
let bounds = if let Some(aside_bounds) = aside_bounds {
|
||
menu_bounds.union(&aside_bounds)
|
||
} else {
|
||
menu_bounds
|
||
};
|
||
return Some(ContextMenuLayout { y_flipped, bounds });
|
||
}
|
||
|
||
None
|
||
}
|
||
|
||
fn layout_gutter_menu(
|
||
&self,
|
||
line_height: Pixels,
|
||
text_hitbox: &Hitbox,
|
||
content_origin: gpui::Point<Pixels>,
|
||
right_margin: Pixels,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
gutter_overshoot: Pixels,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let editor = self.editor.read(cx);
|
||
if !editor.context_menu_visible() {
|
||
return;
|
||
}
|
||
let Some(crate::ContextMenuOrigin::GutterIndicator(gutter_row)) =
|
||
editor.context_menu_origin()
|
||
else {
|
||
return;
|
||
};
|
||
// 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 text field.
|
||
let target_position = content_origin
|
||
+ gpui::Point {
|
||
x: -gutter_overshoot,
|
||
y: Pixels::from(
|
||
gutter_row.next_row().as_f64() * ScrollPixelOffset::from(line_height)
|
||
- scroll_pixel_position.y,
|
||
),
|
||
};
|
||
|
||
let (min_height_in_lines, max_height_in_lines) = editor
|
||
.context_menu_options
|
||
.as_ref()
|
||
.map_or((3, 12), |options| {
|
||
(options.min_entries_visible, options.max_entries_visible)
|
||
});
|
||
|
||
let min_height = line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING;
|
||
let max_height = line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING;
|
||
let viewport_bounds =
|
||
Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
|
||
right: -right_margin - MENU_GAP,
|
||
..Default::default()
|
||
});
|
||
self.layout_popovers_above_or_below_line(
|
||
target_position,
|
||
line_height,
|
||
min_height,
|
||
max_height,
|
||
editor
|
||
.context_menu_options
|
||
.as_ref()
|
||
.and_then(|options| options.placement.clone()),
|
||
text_hitbox,
|
||
viewport_bounds,
|
||
window,
|
||
cx,
|
||
move |height, _max_width_for_stable_x, _, window, cx| {
|
||
let mut element = self
|
||
.render_context_menu(line_height, height, window, cx)
|
||
.expect("Visible context menu should always render.");
|
||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||
vec![(CursorPopoverType::CodeContextMenu, element, size)]
|
||
},
|
||
);
|
||
}
|
||
|
||
fn layout_popovers_above_or_below_line(
|
||
&self,
|
||
target_position: gpui::Point<Pixels>,
|
||
line_height: Pixels,
|
||
min_height: Pixels,
|
||
max_height: Pixels,
|
||
placement: Option<ContextMenuPlacement>,
|
||
text_hitbox: &Hitbox,
|
||
viewport_bounds: Bounds<Pixels>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
make_sized_popovers: impl FnOnce(
|
||
Pixels,
|
||
Pixels,
|
||
bool,
|
||
&mut Window,
|
||
&mut App,
|
||
) -> Vec<(CursorPopoverType, AnyElement, Size<Pixels>)>,
|
||
) -> Option<(Vec<(CursorPopoverType, Bounds<Pixels>)>, bool)> {
|
||
let text_style = TextStyleRefinement {
|
||
line_height: Some(DefiniteLength::Fraction(
|
||
BufferLineHeight::Comfortable.value(),
|
||
)),
|
||
..Default::default()
|
||
};
|
||
window.with_text_style(Some(text_style), |window| {
|
||
// If the max height won't fit below and there is more space above, put it above the line.
|
||
let bottom_y_when_flipped = target_position.y - line_height;
|
||
let available_above = bottom_y_when_flipped - text_hitbox.top();
|
||
let available_below = text_hitbox.bottom() - target_position.y;
|
||
let y_overflows_below = max_height > available_below;
|
||
let mut y_flipped = match placement {
|
||
Some(ContextMenuPlacement::Above) => true,
|
||
Some(ContextMenuPlacement::Below) => false,
|
||
None => y_overflows_below && available_above > available_below,
|
||
};
|
||
let mut height = cmp::min(
|
||
max_height,
|
||
if y_flipped {
|
||
available_above
|
||
} else {
|
||
available_below
|
||
},
|
||
);
|
||
|
||
// If the min height doesn't fit within text bounds, instead fit within the window.
|
||
if height < min_height {
|
||
let available_above = bottom_y_when_flipped;
|
||
let available_below = viewport_bounds.bottom() - target_position.y;
|
||
let (y_flipped_override, height_override) = match placement {
|
||
Some(ContextMenuPlacement::Above) => {
|
||
(true, cmp::min(available_above, min_height))
|
||
}
|
||
Some(ContextMenuPlacement::Below) => {
|
||
(false, cmp::min(available_below, min_height))
|
||
}
|
||
None => {
|
||
if available_below > min_height {
|
||
(false, min_height)
|
||
} else if available_above > min_height {
|
||
(true, min_height)
|
||
} else if available_above > available_below {
|
||
(true, available_above)
|
||
} else {
|
||
(false, available_below)
|
||
}
|
||
}
|
||
};
|
||
y_flipped = y_flipped_override;
|
||
height = height_override;
|
||
}
|
||
|
||
let max_width_for_stable_x = viewport_bounds.right() - target_position.x;
|
||
|
||
// TODO: Use viewport_bounds.width as a max width so that it doesn't get clipped on the left
|
||
// for very narrow windows.
|
||
let popovers =
|
||
make_sized_popovers(height, max_width_for_stable_x, y_flipped, window, cx);
|
||
if popovers.is_empty() {
|
||
return None;
|
||
}
|
||
|
||
let max_width = popovers
|
||
.iter()
|
||
.map(|(_, _, size)| size.width)
|
||
.max()
|
||
.unwrap_or_default();
|
||
|
||
let mut current_position = gpui::Point {
|
||
// Snap the right edge of the list to the right edge of the window if its horizontal bounds
|
||
// overflow. Include space for the scrollbar.
|
||
x: target_position
|
||
.x
|
||
.min((viewport_bounds.right() - max_width).max(Pixels::ZERO)),
|
||
y: if y_flipped {
|
||
bottom_y_when_flipped
|
||
} else {
|
||
target_position.y
|
||
},
|
||
};
|
||
|
||
let mut laid_out_popovers = popovers
|
||
.into_iter()
|
||
.map(|(popover_type, element, size)| {
|
||
if y_flipped {
|
||
current_position.y -= size.height;
|
||
}
|
||
let position = current_position;
|
||
window.defer_draw(element, current_position, 1, None);
|
||
if !y_flipped {
|
||
current_position.y += size.height + MENU_GAP;
|
||
} else {
|
||
current_position.y -= MENU_GAP;
|
||
}
|
||
(popover_type, Bounds::new(position, size))
|
||
})
|
||
.collect::<Vec<_>>();
|
||
|
||
if y_flipped {
|
||
laid_out_popovers.reverse();
|
||
}
|
||
|
||
Some((laid_out_popovers, y_flipped))
|
||
})
|
||
}
|
||
|
||
fn layout_context_menu_aside(
|
||
&self,
|
||
y_flipped: bool,
|
||
menu_bounds: Bounds<Pixels>,
|
||
target_bounds: Bounds<Pixels>,
|
||
max_target_bounds: Bounds<Pixels>,
|
||
max_height: Pixels,
|
||
must_place_above_or_below: bool,
|
||
text_hitbox: &Hitbox,
|
||
viewport_bounds: Bounds<Pixels>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<Bounds<Pixels>> {
|
||
let available_within_viewport = target_bounds.space_within(&viewport_bounds);
|
||
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH
|
||
&& !must_place_above_or_below
|
||
{
|
||
let max_width = cmp::min(
|
||
available_within_viewport.right - px(1.),
|
||
MENU_ASIDE_MAX_WIDTH,
|
||
);
|
||
let mut aside = self.render_context_menu_aside(
|
||
size(max_width, max_height - POPOVER_Y_PADDING),
|
||
window,
|
||
cx,
|
||
)?;
|
||
let size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||
let right_position = point(target_bounds.right(), menu_bounds.origin.y);
|
||
Some((aside, right_position, size))
|
||
} else {
|
||
let max_size = size(
|
||
// TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
|
||
// won't be needed here.
|
||
cmp::min(
|
||
cmp::max(menu_bounds.size.width - px(2.), MENU_ASIDE_MIN_WIDTH),
|
||
viewport_bounds.right(),
|
||
),
|
||
cmp::min(
|
||
max_height,
|
||
cmp::max(
|
||
available_within_viewport.top,
|
||
available_within_viewport.bottom,
|
||
),
|
||
) - POPOVER_Y_PADDING,
|
||
);
|
||
let mut aside = self.render_context_menu_aside(max_size, window, cx)?;
|
||
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||
|
||
let top_position = point(
|
||
menu_bounds.origin.x,
|
||
target_bounds.top() - actual_size.height,
|
||
);
|
||
let bottom_position = point(menu_bounds.origin.x, target_bounds.bottom());
|
||
|
||
let fit_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
|
||
// Prefer to fit on the same side of the line as the menu, then on the other side of
|
||
// the line.
|
||
if !y_flipped && wanted.height < available.bottom {
|
||
Some(bottom_position)
|
||
} else if !y_flipped && wanted.height < available.top {
|
||
Some(top_position)
|
||
} else if y_flipped && wanted.height < available.top {
|
||
Some(top_position)
|
||
} else if y_flipped && wanted.height < available.bottom {
|
||
Some(bottom_position)
|
||
} else {
|
||
None
|
||
}
|
||
};
|
||
|
||
// Prefer choosing a direction using max sizes rather than actual size for stability.
|
||
let available_within_text = max_target_bounds.space_within(&text_hitbox.bounds);
|
||
let wanted = size(MENU_ASIDE_MAX_WIDTH, max_height);
|
||
let aside_position = fit_within(available_within_text, wanted)
|
||
// Fallback: fit max size in window.
|
||
.or_else(|| fit_within(max_target_bounds.space_within(&viewport_bounds), wanted))
|
||
// Fallback: fit actual size in window.
|
||
.or_else(|| fit_within(available_within_viewport, actual_size));
|
||
|
||
aside_position.map(|position| (aside, position, actual_size))
|
||
};
|
||
|
||
// Skip drawing if it doesn't fit anywhere.
|
||
if let Some((aside, position, size)) = positioned_aside {
|
||
let aside_bounds = Bounds::new(position, size);
|
||
window.defer_draw(aside, position, 2, None);
|
||
return Some(aside_bounds);
|
||
}
|
||
|
||
None
|
||
}
|
||
|
||
fn render_context_menu(
|
||
&self,
|
||
line_height: Pixels,
|
||
height: Pixels,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<AnyElement> {
|
||
let max_height_in_lines = ((height - POPOVER_Y_PADDING) / line_height).floor() as u32;
|
||
self.editor.update(cx, |editor, cx| {
|
||
editor.render_context_menu(max_height_in_lines, window, cx)
|
||
})
|
||
}
|
||
|
||
fn render_context_menu_aside(
|
||
&self,
|
||
max_size: Size<Pixels>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<AnyElement> {
|
||
if max_size.width < px(100.) || max_size.height < px(12.) {
|
||
None
|
||
} else {
|
||
self.editor.update(cx, |editor, cx| {
|
||
editor.render_context_menu_aside(max_size, window, cx)
|
||
})
|
||
}
|
||
}
|
||
|
||
fn layout_hover_popovers(
|
||
&self,
|
||
snapshot: &EditorSnapshot,
|
||
hitbox: &Hitbox,
|
||
visible_display_row_range: Range<DisplayRow>,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
line_layouts: &[LineWithInvisibles],
|
||
line_height: Pixels,
|
||
em_width: Pixels,
|
||
context_menu_layout: Option<ContextMenuLayout>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
struct MeasuredHoverPopover {
|
||
element: AnyElement,
|
||
size: Size<Pixels>,
|
||
horizontal_offset: Pixels,
|
||
}
|
||
|
||
let max_size = size(
|
||
(120. * em_width) // Default size
|
||
.min(hitbox.size.width / 2.) // Shrink to half of the editor width
|
||
.max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
|
||
(16. * line_height) // Default size
|
||
.min(hitbox.size.height / 2.) // Shrink to half of the editor height
|
||
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
|
||
);
|
||
|
||
// Don't show hover popovers when context menu is open to avoid overlap
|
||
let has_context_menu = self.editor.read(cx).mouse_context_menu.is_some();
|
||
if has_context_menu {
|
||
return;
|
||
}
|
||
|
||
let hover_popovers = self.editor.update(cx, |editor, cx| {
|
||
editor.hover_state.render(
|
||
snapshot,
|
||
visible_display_row_range.clone(),
|
||
max_size,
|
||
&editor.text_layout_details(window, cx),
|
||
window,
|
||
cx,
|
||
)
|
||
});
|
||
let Some((popover_position, hover_popovers)) = hover_popovers else {
|
||
return;
|
||
};
|
||
|
||
// This is safe because we check on layout whether the required row is available
|
||
let hovered_row_layout = &line_layouts[popover_position
|
||
.row()
|
||
.minus(visible_display_row_range.start)
|
||
as usize];
|
||
|
||
// Compute Hovered Point
|
||
let x = hovered_row_layout.x_for_index(popover_position.column() as usize)
|
||
- Pixels::from(scroll_pixel_position.x);
|
||
let y = Pixels::from(
|
||
popover_position.row().as_f64() * ScrollPixelOffset::from(line_height)
|
||
- scroll_pixel_position.y,
|
||
);
|
||
let hovered_point = content_origin + point(x, y);
|
||
|
||
let mut overall_height = Pixels::ZERO;
|
||
let mut measured_hover_popovers = Vec::new();
|
||
for (position, mut hover_popover) in hover_popovers.into_iter().with_position() {
|
||
let size = hover_popover.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||
let horizontal_offset =
|
||
(hitbox.top_right().x - POPOVER_RIGHT_OFFSET - (hovered_point.x + size.width))
|
||
.min(Pixels::ZERO);
|
||
match position {
|
||
itertools::Position::Middle | itertools::Position::Last => {
|
||
overall_height += HOVER_POPOVER_GAP
|
||
}
|
||
_ => {}
|
||
}
|
||
overall_height += size.height;
|
||
measured_hover_popovers.push(MeasuredHoverPopover {
|
||
element: hover_popover,
|
||
size,
|
||
horizontal_offset,
|
||
});
|
||
}
|
||
|
||
fn draw_occluder(
|
||
width: Pixels,
|
||
origin: gpui::Point<Pixels>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let mut occlusion = div()
|
||
.size_full()
|
||
.occlude()
|
||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||
.into_any_element();
|
||
occlusion.layout_as_root(size(width, HOVER_POPOVER_GAP).into(), window, cx);
|
||
window.defer_draw(occlusion, origin, 2, None);
|
||
}
|
||
|
||
fn place_popovers_above(
|
||
hovered_point: gpui::Point<Pixels>,
|
||
measured_hover_popovers: Vec<MeasuredHoverPopover>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let mut current_y = hovered_point.y;
|
||
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
|
||
let size = popover.size;
|
||
let popover_origin = point(
|
||
hovered_point.x + popover.horizontal_offset,
|
||
current_y - size.height,
|
||
);
|
||
|
||
window.defer_draw(popover.element, popover_origin, 2, None);
|
||
if position != itertools::Position::Last {
|
||
let origin = point(popover_origin.x, popover_origin.y - HOVER_POPOVER_GAP);
|
||
draw_occluder(size.width, origin, window, cx);
|
||
}
|
||
|
||
current_y = popover_origin.y - HOVER_POPOVER_GAP;
|
||
}
|
||
}
|
||
|
||
fn place_popovers_below(
|
||
hovered_point: gpui::Point<Pixels>,
|
||
measured_hover_popovers: Vec<MeasuredHoverPopover>,
|
||
line_height: Pixels,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let mut current_y = hovered_point.y + line_height;
|
||
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
|
||
let size = popover.size;
|
||
let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
|
||
|
||
window.defer_draw(popover.element, popover_origin, 2, None);
|
||
if position != itertools::Position::Last {
|
||
let origin = point(popover_origin.x, popover_origin.y + size.height);
|
||
draw_occluder(size.width, origin, window, cx);
|
||
}
|
||
|
||
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
|
||
}
|
||
}
|
||
|
||
let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
|
||
context_menu_layout
|
||
.as_ref()
|
||
.is_some_and(|menu| bounds.intersects(&menu.bounds))
|
||
};
|
||
|
||
let can_place_above = {
|
||
let mut bounds_above = Vec::new();
|
||
let mut current_y = hovered_point.y;
|
||
for popover in &measured_hover_popovers {
|
||
let size = popover.size;
|
||
let popover_origin = point(
|
||
hovered_point.x + popover.horizontal_offset,
|
||
current_y - size.height,
|
||
);
|
||
bounds_above.push(Bounds::new(popover_origin, size));
|
||
current_y = popover_origin.y - HOVER_POPOVER_GAP;
|
||
}
|
||
bounds_above
|
||
.iter()
|
||
.all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
|
||
};
|
||
|
||
let can_place_below = || {
|
||
let mut bounds_below = Vec::new();
|
||
let mut current_y = hovered_point.y + line_height;
|
||
for popover in &measured_hover_popovers {
|
||
let size = popover.size;
|
||
let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
|
||
bounds_below.push(Bounds::new(popover_origin, size));
|
||
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
|
||
}
|
||
bounds_below
|
||
.iter()
|
||
.all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
|
||
};
|
||
|
||
if can_place_above {
|
||
// try placing above hovered point
|
||
place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
|
||
} else if can_place_below() {
|
||
// try placing below hovered point
|
||
place_popovers_below(
|
||
hovered_point,
|
||
measured_hover_popovers,
|
||
line_height,
|
||
window,
|
||
cx,
|
||
);
|
||
} else {
|
||
// try to place popovers around the context menu
|
||
let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
|
||
let total_width = measured_hover_popovers
|
||
.iter()
|
||
.map(|p| p.size.width)
|
||
.max()
|
||
.unwrap_or(Pixels::ZERO);
|
||
let y_for_horizontal_positioning = if menu.y_flipped {
|
||
menu.bounds.bottom() - overall_height
|
||
} else {
|
||
menu.bounds.top()
|
||
};
|
||
let possible_origins = vec![
|
||
// left of context menu
|
||
point(
|
||
menu.bounds.left() - total_width - HOVER_POPOVER_GAP,
|
||
y_for_horizontal_positioning,
|
||
),
|
||
// right of context menu
|
||
point(
|
||
menu.bounds.right() + HOVER_POPOVER_GAP,
|
||
y_for_horizontal_positioning,
|
||
),
|
||
// top of context menu
|
||
point(
|
||
menu.bounds.left(),
|
||
menu.bounds.top() - overall_height - HOVER_POPOVER_GAP,
|
||
),
|
||
// bottom of context menu
|
||
point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
|
||
];
|
||
possible_origins.into_iter().find(|&origin| {
|
||
Bounds::new(origin, size(total_width, overall_height))
|
||
.is_contained_within(hitbox)
|
||
})
|
||
});
|
||
if let Some(origin) = origin_surrounding_menu {
|
||
let mut current_y = origin.y;
|
||
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
|
||
let size = popover.size;
|
||
let popover_origin = point(origin.x, current_y);
|
||
|
||
window.defer_draw(popover.element, popover_origin, 2, None);
|
||
if position != itertools::Position::Last {
|
||
let origin = point(popover_origin.x, popover_origin.y + size.height);
|
||
draw_occluder(size.width, origin, window, cx);
|
||
}
|
||
|
||
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
|
||
}
|
||
} else {
|
||
// fallback to existing above/below cursor logic
|
||
// this might overlap menu or overflow in rare case
|
||
if can_place_above {
|
||
place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
|
||
} else {
|
||
place_popovers_below(
|
||
hovered_point,
|
||
measured_hover_popovers,
|
||
line_height,
|
||
window,
|
||
cx,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn layout_word_diff_highlights(
|
||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||
row_infos: &[RowInfo],
|
||
start_row: DisplayRow,
|
||
snapshot: &EditorSnapshot,
|
||
highlighted_ranges: &mut Vec<(Range<DisplayPoint>, Hsla)>,
|
||
cx: &mut App,
|
||
) {
|
||
let colors = cx.theme().colors();
|
||
|
||
let word_highlights = display_hunks
|
||
.into_iter()
|
||
.filter_map(|(hunk, _)| match hunk {
|
||
DisplayDiffHunk::Unfolded {
|
||
word_diffs, status, ..
|
||
} => Some((word_diffs, status)),
|
||
_ => None,
|
||
})
|
||
.filter(|(_, status)| status.is_modified())
|
||
.flat_map(|(word_diffs, _)| word_diffs)
|
||
.flat_map(|word_diff| {
|
||
let display_ranges = snapshot
|
||
.display_snapshot
|
||
.isomorphic_display_point_ranges_for_buffer_range(
|
||
word_diff.start..word_diff.end,
|
||
);
|
||
|
||
display_ranges.into_iter().filter_map(|range| {
|
||
let start_row_offset = range.start.row().0.saturating_sub(start_row.0) as usize;
|
||
|
||
let diff_status = row_infos
|
||
.get(start_row_offset)
|
||
.and_then(|row_info| row_info.diff_status)?;
|
||
|
||
let background_color = match diff_status.kind {
|
||
DiffHunkStatusKind::Added => colors.version_control_word_added,
|
||
DiffHunkStatusKind::Deleted => colors.version_control_word_deleted,
|
||
DiffHunkStatusKind::Modified => {
|
||
debug_panic!("modified diff status for row info");
|
||
return None;
|
||
}
|
||
};
|
||
|
||
Some((range, background_color))
|
||
})
|
||
});
|
||
|
||
highlighted_ranges.extend(word_highlights);
|
||
}
|
||
|
||
fn layout_diff_hunk_controls(
|
||
&self,
|
||
row_range: Range<DisplayRow>,
|
||
row_infos: &[RowInfo],
|
||
text_hitbox: &Hitbox,
|
||
newest_cursor_row: Option<DisplayRow>,
|
||
line_height: Pixels,
|
||
right_margin: Pixels,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
sticky_header_height: Pixels,
|
||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||
highlighted_rows: &BTreeMap<DisplayRow, LineHighlight>,
|
||
editor: Entity<Editor>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> (Vec<AnyElement>, Vec<(DisplayRow, Bounds<Pixels>)>) {
|
||
let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone();
|
||
let hovered_diff_hunk_row = editor.read(cx).hovered_diff_hunk_row;
|
||
let sticky_top = text_hitbox.bounds.top() + sticky_header_height;
|
||
|
||
let mut controls = vec![];
|
||
let mut control_bounds = vec![];
|
||
|
||
let active_rows = [hovered_diff_hunk_row, newest_cursor_row];
|
||
|
||
for (hunk, _) in display_hunks {
|
||
if let DisplayDiffHunk::Unfolded {
|
||
display_row_range,
|
||
multi_buffer_range,
|
||
status,
|
||
is_created_file,
|
||
..
|
||
} = &hunk
|
||
{
|
||
if display_row_range.start >= row_range.end {
|
||
// hunk is fully below the viewport
|
||
continue;
|
||
}
|
||
if display_row_range.end <= row_range.start {
|
||
// hunk is fully above the viewport
|
||
continue;
|
||
}
|
||
let row_ix = display_row_range.start.0.saturating_sub(row_range.start.0);
|
||
if row_infos
|
||
.get(row_ix as usize)
|
||
.and_then(|row_info| row_info.diff_status)
|
||
.is_none()
|
||
{
|
||
continue;
|
||
}
|
||
if highlighted_rows
|
||
.get(&display_row_range.start)
|
||
.and_then(|highlight| highlight.type_id)
|
||
.is_some_and(|type_id| {
|
||
[
|
||
TypeId::of::<ConflictsOuter>(),
|
||
TypeId::of::<ConflictsOursMarker>(),
|
||
TypeId::of::<ConflictsOurs>(),
|
||
TypeId::of::<ConflictsTheirs>(),
|
||
TypeId::of::<ConflictsTheirsMarker>(),
|
||
]
|
||
.contains(&type_id)
|
||
})
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if active_rows
|
||
.iter()
|
||
.any(|row| row.is_some_and(|row| display_row_range.contains(&row)))
|
||
{
|
||
let hunk_start_y: Pixels = (display_row_range.start.as_f64()
|
||
* ScrollPixelOffset::from(line_height)
|
||
+ ScrollPixelOffset::from(text_hitbox.bounds.top())
|
||
- scroll_pixel_position.y)
|
||
.into();
|
||
|
||
let y: Pixels = if hunk_start_y >= sticky_top {
|
||
hunk_start_y
|
||
} else {
|
||
let hunk_end_y: Pixels = hunk_start_y
|
||
+ (display_row_range.len() as f64
|
||
* ScrollPixelOffset::from(line_height))
|
||
.into();
|
||
let max_y = hunk_end_y - line_height;
|
||
sticky_top.min(max_y)
|
||
};
|
||
|
||
let mut element = render_diff_hunk_controls(
|
||
display_row_range.start.0,
|
||
status,
|
||
multi_buffer_range.clone(),
|
||
*is_created_file,
|
||
line_height,
|
||
&editor,
|
||
window,
|
||
cx,
|
||
);
|
||
let size =
|
||
element.layout_as_root(size(px(100.0), line_height).into(), window, cx);
|
||
|
||
let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width;
|
||
|
||
if x < text_hitbox.bounds.left() {
|
||
continue;
|
||
}
|
||
|
||
let bounds = Bounds::new(gpui::Point::new(x, y), size);
|
||
control_bounds.push((display_row_range.start, bounds));
|
||
|
||
window.with_absolute_element_offset(gpui::Point::new(x, y), |window| {
|
||
element.prepaint(window, cx)
|
||
});
|
||
controls.push(element);
|
||
}
|
||
}
|
||
}
|
||
|
||
(controls, control_bounds)
|
||
}
|
||
|
||
fn layout_signature_help(
|
||
&self,
|
||
hitbox: &Hitbox,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
newest_selection_head: Option<DisplayPoint>,
|
||
start_row: DisplayRow,
|
||
line_layouts: &[LineWithInvisibles],
|
||
line_height: Pixels,
|
||
em_width: Pixels,
|
||
context_menu_layout: Option<ContextMenuLayout>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
if !self.editor.focus_handle(cx).is_focused(window) {
|
||
return;
|
||
}
|
||
let Some(newest_selection_head) = newest_selection_head else {
|
||
return;
|
||
};
|
||
|
||
let max_size = size(
|
||
(120. * em_width) // Default size
|
||
.min(hitbox.size.width / 2.) // Shrink to half of the editor width
|
||
.max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
|
||
(16. * line_height) // Default size
|
||
.min(hitbox.size.height / 2.) // Shrink to half of the editor height
|
||
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
|
||
);
|
||
|
||
let maybe_element = self.editor.update(cx, |editor, cx| {
|
||
if let Some(popover) = editor.signature_help_state.popover_mut() {
|
||
let element = popover.render(max_size, window, cx);
|
||
Some(element)
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
let Some(mut element) = maybe_element else {
|
||
return;
|
||
};
|
||
|
||
let selection_row = newest_selection_head.row();
|
||
let Some(cursor_row_layout) = (selection_row >= start_row)
|
||
.then(|| line_layouts.get(selection_row.minus(start_row) as usize))
|
||
.flatten()
|
||
else {
|
||
return;
|
||
};
|
||
|
||
let target_x = cursor_row_layout.x_for_index(newest_selection_head.column() as usize)
|
||
- Pixels::from(scroll_pixel_position.x);
|
||
let target_y = Pixels::from(
|
||
selection_row.as_f64() * ScrollPixelOffset::from(line_height) - scroll_pixel_position.y,
|
||
);
|
||
let target_point = content_origin + point(target_x, target_y);
|
||
|
||
let actual_size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
|
||
|
||
let (popover_bounds_above, popover_bounds_below) = {
|
||
let horizontal_offset = (hitbox.top_right().x
|
||
- POPOVER_RIGHT_OFFSET
|
||
- (target_point.x + actual_size.width))
|
||
.min(Pixels::ZERO);
|
||
let initial_x = target_point.x + horizontal_offset;
|
||
(
|
||
Bounds::new(
|
||
point(initial_x, target_point.y - actual_size.height),
|
||
actual_size,
|
||
),
|
||
Bounds::new(
|
||
point(initial_x, target_point.y + line_height + HOVER_POPOVER_GAP),
|
||
actual_size,
|
||
),
|
||
)
|
||
};
|
||
|
||
let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
|
||
context_menu_layout
|
||
.as_ref()
|
||
.is_some_and(|menu| bounds.intersects(&menu.bounds))
|
||
};
|
||
|
||
let final_origin = if popover_bounds_above.is_contained_within(hitbox)
|
||
&& !intersects_menu(popover_bounds_above)
|
||
{
|
||
// try placing above cursor
|
||
popover_bounds_above.origin
|
||
} else if popover_bounds_below.is_contained_within(hitbox)
|
||
&& !intersects_menu(popover_bounds_below)
|
||
{
|
||
// try placing below cursor
|
||
popover_bounds_below.origin
|
||
} else {
|
||
// try surrounding context menu if exists
|
||
let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
|
||
let y_for_horizontal_positioning = if menu.y_flipped {
|
||
menu.bounds.bottom() - actual_size.height
|
||
} else {
|
||
menu.bounds.top()
|
||
};
|
||
let possible_origins = vec![
|
||
// left of context menu
|
||
point(
|
||
menu.bounds.left() - actual_size.width - HOVER_POPOVER_GAP,
|
||
y_for_horizontal_positioning,
|
||
),
|
||
// right of context menu
|
||
point(
|
||
menu.bounds.right() + HOVER_POPOVER_GAP,
|
||
y_for_horizontal_positioning,
|
||
),
|
||
// top of context menu
|
||
point(
|
||
menu.bounds.left(),
|
||
menu.bounds.top() - actual_size.height - HOVER_POPOVER_GAP,
|
||
),
|
||
// bottom of context menu
|
||
point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
|
||
];
|
||
possible_origins
|
||
.into_iter()
|
||
.find(|&origin| Bounds::new(origin, actual_size).is_contained_within(hitbox))
|
||
});
|
||
origin_surrounding_menu.unwrap_or_else(|| {
|
||
// fallback to existing above/below cursor logic
|
||
// this might overlap menu or overflow in rare case
|
||
if popover_bounds_above.is_contained_within(hitbox) {
|
||
popover_bounds_above.origin
|
||
} else {
|
||
popover_bounds_below.origin
|
||
}
|
||
})
|
||
};
|
||
|
||
window.defer_draw(element, final_origin, 2, None);
|
||
}
|
||
|
||
fn paint_background(&self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
|
||
window.paint_layer(layout.hitbox.bounds, |window| {
|
||
let scroll_top = layout.position_map.scroll_position.y;
|
||
let gutter_bg = cx.theme().colors().editor_gutter_background;
|
||
window.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg));
|
||
window.paint_quad(fill(
|
||
layout.position_map.text_hitbox.bounds,
|
||
self.style.background,
|
||
));
|
||
|
||
if matches!(
|
||
layout.mode,
|
||
EditorMode::Full { .. } | EditorMode::Minimap { .. }
|
||
) {
|
||
let show_active_line_background = match layout.mode {
|
||
EditorMode::Full {
|
||
show_active_line_background,
|
||
..
|
||
} => show_active_line_background,
|
||
EditorMode::Minimap { .. } => true,
|
||
_ => false,
|
||
};
|
||
let mut active_rows = layout.active_rows.iter().peekable();
|
||
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
|
||
let mut end_row = start_row.0;
|
||
while active_rows
|
||
.peek()
|
||
.is_some_and(|(active_row, has_selection)| {
|
||
active_row.0 == end_row + 1
|
||
&& has_selection.selection == contains_non_empty_selection.selection
|
||
})
|
||
{
|
||
active_rows.next().unwrap();
|
||
end_row += 1;
|
||
}
|
||
|
||
if show_active_line_background && !contains_non_empty_selection.selection {
|
||
let highlight_h_range =
|
||
match layout.position_map.snapshot.current_line_highlight {
|
||
CurrentLineHighlight::Gutter => Some(Range {
|
||
start: layout.hitbox.left(),
|
||
end: layout.gutter_hitbox.right(),
|
||
}),
|
||
CurrentLineHighlight::Line => Some(Range {
|
||
start: layout.position_map.text_hitbox.bounds.left(),
|
||
end: layout.position_map.text_hitbox.bounds.right(),
|
||
}),
|
||
CurrentLineHighlight::All => Some(Range {
|
||
start: layout.hitbox.left(),
|
||
end: layout.hitbox.right(),
|
||
}),
|
||
CurrentLineHighlight::None => None,
|
||
};
|
||
if let Some(range) = highlight_h_range {
|
||
let active_line_bg = cx.theme().colors().editor_active_line_background;
|
||
let bounds = Bounds {
|
||
origin: point(
|
||
range.start,
|
||
layout.hitbox.origin.y
|
||
+ Pixels::from(
|
||
(start_row.as_f64() - scroll_top)
|
||
* ScrollPixelOffset::from(
|
||
layout.position_map.line_height,
|
||
),
|
||
),
|
||
),
|
||
size: size(
|
||
range.end - range.start,
|
||
layout.position_map.line_height
|
||
* (end_row - start_row.0 + 1) as f32,
|
||
),
|
||
};
|
||
window.paint_quad(fill(bounds, active_line_bg));
|
||
}
|
||
}
|
||
}
|
||
|
||
let mut paint_highlight = |highlight_row_start: DisplayRow,
|
||
highlight_row_end: DisplayRow,
|
||
highlight: crate::LineHighlight,
|
||
edges| {
|
||
let mut origin_x = layout.hitbox.left();
|
||
let mut width = layout.hitbox.size.width;
|
||
if !highlight.include_gutter {
|
||
origin_x += layout.gutter_hitbox.size.width;
|
||
width -= layout.gutter_hitbox.size.width;
|
||
}
|
||
|
||
let origin = point(
|
||
origin_x,
|
||
layout.hitbox.origin.y
|
||
+ Pixels::from(
|
||
(highlight_row_start.as_f64() - scroll_top)
|
||
* ScrollPixelOffset::from(layout.position_map.line_height),
|
||
),
|
||
);
|
||
let size = size(
|
||
width,
|
||
layout.position_map.line_height
|
||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||
);
|
||
let mut quad = fill(Bounds { origin, size }, highlight.background);
|
||
if let Some(border_color) = highlight.border {
|
||
quad.border_color = border_color;
|
||
quad.border_widths = edges
|
||
}
|
||
window.paint_quad(quad);
|
||
};
|
||
|
||
let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
|
||
None;
|
||
for (&new_row, &new_background) in &layout.highlighted_rows {
|
||
match &mut current_paint {
|
||
&mut Some((current_background, ref mut current_range, mut edges)) => {
|
||
let new_range_started = current_background != new_background
|
||
|| current_range.end.next_row() != new_row;
|
||
if new_range_started {
|
||
if current_range.end.next_row() == new_row {
|
||
edges.bottom = px(0.);
|
||
};
|
||
paint_highlight(
|
||
current_range.start,
|
||
current_range.end,
|
||
current_background,
|
||
edges,
|
||
);
|
||
let edges = Edges {
|
||
top: if current_range.end.next_row() != new_row {
|
||
px(1.)
|
||
} else {
|
||
px(0.)
|
||
},
|
||
bottom: px(1.),
|
||
..Default::default()
|
||
};
|
||
current_paint = Some((new_background, new_row..new_row, edges));
|
||
continue;
|
||
} else {
|
||
current_range.end = current_range.end.next_row();
|
||
}
|
||
}
|
||
None => {
|
||
let edges = Edges {
|
||
top: px(1.),
|
||
bottom: px(1.),
|
||
..Default::default()
|
||
};
|
||
current_paint = Some((new_background, new_row..new_row, edges))
|
||
}
|
||
};
|
||
}
|
||
if let Some((color, range, edges)) = current_paint {
|
||
paint_highlight(range.start, range.end, color, edges);
|
||
}
|
||
|
||
for (guide_x, active) in layout.wrap_guides.iter() {
|
||
let color = if *active {
|
||
cx.theme().colors().editor_active_wrap_guide
|
||
} else {
|
||
cx.theme().colors().editor_wrap_guide
|
||
};
|
||
window.paint_quad(fill(
|
||
window.pixel_snap_bounds(Bounds {
|
||
origin: point(*guide_x, layout.position_map.text_hitbox.origin.y),
|
||
size: size(px(1.), layout.position_map.text_hitbox.size.height),
|
||
}),
|
||
color,
|
||
));
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
fn paint_indent_guides(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let Some(indent_guides) = &layout.indent_guides else {
|
||
return;
|
||
};
|
||
|
||
let faded_color = |color: Hsla, alpha: f32| {
|
||
let mut faded = color;
|
||
faded.a = alpha;
|
||
faded
|
||
};
|
||
|
||
for indent_guide in indent_guides {
|
||
let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
|
||
let settings = &indent_guide.settings;
|
||
|
||
// TODO fixed for now, expose them through themes later
|
||
const INDENT_AWARE_ALPHA: f32 = 0.2;
|
||
const INDENT_AWARE_ACTIVE_ALPHA: f32 = 0.4;
|
||
const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
|
||
const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
|
||
|
||
let line_color = match (settings.coloring, indent_guide.active) {
|
||
(IndentGuideColoring::Disabled, _) => None,
|
||
(IndentGuideColoring::Fixed, false) => {
|
||
Some(cx.theme().colors().editor_indent_guide)
|
||
}
|
||
(IndentGuideColoring::Fixed, true) => {
|
||
Some(cx.theme().colors().editor_indent_guide_active)
|
||
}
|
||
(IndentGuideColoring::IndentAware, false) => {
|
||
Some(faded_color(indent_accent_colors, INDENT_AWARE_ALPHA))
|
||
}
|
||
(IndentGuideColoring::IndentAware, true) => {
|
||
Some(faded_color(indent_accent_colors, INDENT_AWARE_ACTIVE_ALPHA))
|
||
}
|
||
};
|
||
|
||
let background_color = match (settings.background_coloring, indent_guide.active) {
|
||
(IndentGuideBackgroundColoring::Disabled, _) => None,
|
||
(IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
|
||
indent_accent_colors,
|
||
INDENT_AWARE_BACKGROUND_ALPHA,
|
||
)),
|
||
(IndentGuideBackgroundColoring::IndentAware, true) => Some(faded_color(
|
||
indent_accent_colors,
|
||
INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA,
|
||
)),
|
||
};
|
||
|
||
let mut line_indicator_width = 0.;
|
||
if let Some(requested_line_width) = settings.visible_line_width(indent_guide.active) {
|
||
if let Some(color) = line_color {
|
||
window.paint_quad(fill(
|
||
window.pixel_snap_bounds(Bounds {
|
||
origin: indent_guide.origin,
|
||
size: size(px(requested_line_width as f32), indent_guide.length),
|
||
}),
|
||
color,
|
||
));
|
||
line_indicator_width = requested_line_width as f32;
|
||
}
|
||
}
|
||
|
||
if let Some(color) = background_color {
|
||
let width = indent_guide.single_indent_width - px(line_indicator_width);
|
||
window.paint_quad(fill(
|
||
window.pixel_snap_bounds(Bounds {
|
||
origin: point(
|
||
indent_guide.origin.x + px(line_indicator_width),
|
||
indent_guide.origin.y,
|
||
),
|
||
size: size(width, indent_guide.length),
|
||
}),
|
||
color,
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
fn paint_line_numbers(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||
let is_singleton = self.editor.read(cx).buffer_kind(cx) == ItemBufferKind::Singleton;
|
||
|
||
let line_height = layout.position_map.line_height;
|
||
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
|
||
|
||
for line_layout in layout.line_numbers.values() {
|
||
for LineNumberSegment {
|
||
shaped_line,
|
||
hitbox,
|
||
} in &line_layout.segments
|
||
{
|
||
let Some(hitbox) = hitbox else {
|
||
continue;
|
||
};
|
||
|
||
let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
|
||
let color = cx.theme().colors().editor_hover_line_number;
|
||
|
||
let line = self.shape_line_number(shaped_line.text.clone(), color, window);
|
||
line.paint(
|
||
hitbox.origin,
|
||
line_height,
|
||
TextAlign::Left,
|
||
None,
|
||
window,
|
||
cx,
|
||
)
|
||
.log_err()
|
||
} else {
|
||
shaped_line
|
||
.paint(
|
||
hitbox.origin,
|
||
line_height,
|
||
TextAlign::Left,
|
||
None,
|
||
window,
|
||
cx,
|
||
)
|
||
.log_err()
|
||
}) else {
|
||
continue;
|
||
};
|
||
|
||
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
|
||
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
|
||
if is_singleton {
|
||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||
} else {
|
||
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn paint_gutter_diff_hunks(
|
||
layout: &mut EditorLayout,
|
||
split_side: Option<SplitSide>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
if layout.display_hunks.is_empty() {
|
||
return;
|
||
}
|
||
|
||
let line_height = layout.position_map.line_height;
|
||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||
for (hunk, hitbox) in &layout.display_hunks {
|
||
let hunk_to_paint = match hunk {
|
||
DisplayDiffHunk::Folded { .. } => {
|
||
let hunk_bounds = Self::diff_hunk_bounds(
|
||
layout.position_map.scroll_position,
|
||
line_height,
|
||
layout.gutter_hitbox.bounds,
|
||
hunk,
|
||
&layout.position_map.snapshot,
|
||
);
|
||
Some((
|
||
hunk_bounds,
|
||
cx.theme().colors().version_control_modified,
|
||
Corners::all(px(0.)),
|
||
DiffHunkStatus::modified_none(),
|
||
))
|
||
}
|
||
DisplayDiffHunk::Unfolded {
|
||
status,
|
||
display_row_range,
|
||
..
|
||
} => hitbox.as_ref().map(|hunk_hitbox| {
|
||
let color = match split_side {
|
||
Some(SplitSide::Left) => cx.theme().colors().version_control_deleted,
|
||
Some(SplitSide::Right) => cx.theme().colors().version_control_added,
|
||
None => match status.kind {
|
||
DiffHunkStatusKind::Added => {
|
||
cx.theme().colors().version_control_added
|
||
}
|
||
DiffHunkStatusKind::Modified => {
|
||
cx.theme().colors().version_control_modified
|
||
}
|
||
DiffHunkStatusKind::Deleted => {
|
||
cx.theme().colors().version_control_deleted
|
||
}
|
||
},
|
||
};
|
||
match status.kind {
|
||
DiffHunkStatusKind::Deleted if display_row_range.is_empty() => (
|
||
Bounds::new(
|
||
point(
|
||
hunk_hitbox.origin.x - hunk_hitbox.size.width,
|
||
hunk_hitbox.origin.y,
|
||
),
|
||
size(hunk_hitbox.size.width * 2., hunk_hitbox.size.height),
|
||
),
|
||
color,
|
||
Corners::all(1. * line_height),
|
||
*status,
|
||
),
|
||
_ => (hunk_hitbox.bounds, color, Corners::all(px(0.)), *status),
|
||
}
|
||
}),
|
||
};
|
||
|
||
if let Some((hunk_bounds, background_color, corner_radii, status)) = hunk_to_paint {
|
||
// Flatten the background color with the editor color to prevent
|
||
// elements below transparent hunks from showing through
|
||
let flattened_background_color = cx
|
||
.theme()
|
||
.colors()
|
||
.editor_background
|
||
.blend(background_color);
|
||
|
||
if !Self::diff_hunk_hollow(status, cx) {
|
||
window.paint_quad(quad(
|
||
hunk_bounds,
|
||
corner_radii,
|
||
flattened_background_color,
|
||
Edges::default(),
|
||
transparent_black(),
|
||
BorderStyle::default(),
|
||
));
|
||
} else {
|
||
let flattened_unstaged_background_color = cx
|
||
.theme()
|
||
.colors()
|
||
.editor_background
|
||
.blend(background_color.opacity(0.3));
|
||
|
||
window.paint_quad(quad(
|
||
hunk_bounds,
|
||
corner_radii,
|
||
flattened_unstaged_background_color,
|
||
Edges::all(px(1.0)),
|
||
flattened_background_color,
|
||
BorderStyle::Solid,
|
||
));
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
fn gutter_strip_width(line_height: Pixels) -> Pixels {
|
||
(0.275 * line_height).floor()
|
||
}
|
||
|
||
fn diff_hunk_bounds(
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
line_height: Pixels,
|
||
gutter_bounds: Bounds<Pixels>,
|
||
hunk: &DisplayDiffHunk,
|
||
snapshot: &EditorSnapshot,
|
||
) -> Bounds<Pixels> {
|
||
let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
|
||
let gutter_strip_width = Self::gutter_strip_width(line_height);
|
||
|
||
match hunk {
|
||
DisplayDiffHunk::Folded { display_row, .. } => {
|
||
let start_y = (display_row.as_f64() * ScrollPixelOffset::from(line_height)
|
||
- scroll_top)
|
||
.into();
|
||
let end_y = start_y + line_height;
|
||
let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
|
||
let highlight_size = size(gutter_strip_width, end_y - start_y);
|
||
Bounds::new(highlight_origin, highlight_size)
|
||
}
|
||
DisplayDiffHunk::Unfolded {
|
||
display_row_range,
|
||
status,
|
||
..
|
||
} => {
|
||
if status.is_deleted() && display_row_range.is_empty() {
|
||
let row = display_row_range.start;
|
||
|
||
let offset = ScrollPixelOffset::from(line_height / 2.);
|
||
let start_y =
|
||
(row.as_f64() * ScrollPixelOffset::from(line_height) - offset - scroll_top)
|
||
.into();
|
||
let end_y = start_y + line_height;
|
||
|
||
let width = (0.35 * line_height).floor();
|
||
let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
|
||
let highlight_size = size(width, end_y - start_y);
|
||
Bounds::new(highlight_origin, highlight_size)
|
||
} else {
|
||
let start_row = display_row_range.start;
|
||
let end_row = display_row_range.end;
|
||
// If we're in a multibuffer, row range span might include an
|
||
// excerpt header, so if we were to draw the marker straight away,
|
||
// the hunk might include the rows of that header.
|
||
// Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
|
||
// Instead, we simply check whether the range we're dealing with includes
|
||
// any excerpt headers and if so, we stop painting the diff hunk on the first row of that header.
|
||
let end_row_in_current_excerpt = snapshot
|
||
.blocks_in_range(start_row..end_row)
|
||
.find_map(|(start_row, block)| {
|
||
if matches!(
|
||
block,
|
||
Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
|
||
) {
|
||
Some(start_row)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.unwrap_or(end_row);
|
||
|
||
let start_y = (start_row.as_f64() * ScrollPixelOffset::from(line_height)
|
||
- scroll_top)
|
||
.into();
|
||
let end_y = Pixels::from(
|
||
end_row_in_current_excerpt.as_f64() * ScrollPixelOffset::from(line_height)
|
||
- scroll_top,
|
||
);
|
||
|
||
let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
|
||
let highlight_size = size(gutter_strip_width, end_y - start_y);
|
||
Bounds::new(highlight_origin, highlight_size)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn paint_gutter_indicators(
|
||
&self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||
window.with_element_namespace("crease_toggles", |window| {
|
||
for crease_toggle in layout.crease_toggles.iter_mut().flatten() {
|
||
crease_toggle.paint(window, cx);
|
||
}
|
||
});
|
||
|
||
window.with_element_namespace("expand_toggles", |window| {
|
||
for (expand_toggle, _) in layout.expand_toggles.iter_mut().flatten() {
|
||
expand_toggle.paint(window, cx);
|
||
}
|
||
});
|
||
|
||
for bookmark in layout.bookmarks.iter_mut() {
|
||
bookmark.paint(window, cx);
|
||
}
|
||
|
||
for breakpoint in layout.breakpoints.iter_mut() {
|
||
breakpoint.paint(window, cx);
|
||
}
|
||
|
||
for test_indicator in layout.test_indicators.iter_mut() {
|
||
test_indicator.paint(window, cx);
|
||
}
|
||
|
||
if let Some(diff_review_button) = layout.diff_review_button.as_mut() {
|
||
diff_review_button.paint(window, cx);
|
||
}
|
||
});
|
||
}
|
||
|
||
fn paint_gutter_highlights(
|
||
&self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for (_, hunk_hitbox) in &layout.display_hunks {
|
||
if let Some(hunk_hitbox) = hunk_hitbox
|
||
&& !self
|
||
.editor
|
||
.read(cx)
|
||
.buffer()
|
||
.read(cx)
|
||
.all_diff_hunks_expanded()
|
||
{
|
||
window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
|
||
}
|
||
}
|
||
|
||
let show_git_gutter = layout
|
||
.position_map
|
||
.snapshot
|
||
.show_git_diff_gutter
|
||
.unwrap_or_else(|| {
|
||
matches!(
|
||
ProjectSettings::get_global(cx).git.git_gutter,
|
||
GitGutterSetting::TrackedFiles
|
||
)
|
||
});
|
||
if show_git_gutter {
|
||
Self::paint_gutter_diff_hunks(layout, self.split_side, window, cx)
|
||
}
|
||
|
||
let highlight_width = 0.275 * layout.position_map.line_height;
|
||
let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
|
||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||
for (range, color) in &layout.highlighted_gutter_ranges {
|
||
let start_row = if range.start.row() < layout.visible_display_row_range.start {
|
||
layout.visible_display_row_range.start - DisplayRow(1)
|
||
} else {
|
||
range.start.row()
|
||
};
|
||
let end_row = if range.end.row() > layout.visible_display_row_range.end {
|
||
layout.visible_display_row_range.end + DisplayRow(1)
|
||
} else {
|
||
range.end.row()
|
||
};
|
||
|
||
let start_y = layout.gutter_hitbox.top()
|
||
+ Pixels::from(
|
||
start_row.0 as f64
|
||
* ScrollPixelOffset::from(layout.position_map.line_height)
|
||
- layout.position_map.scroll_pixel_position.y,
|
||
);
|
||
let end_y = layout.gutter_hitbox.top()
|
||
+ Pixels::from(
|
||
(end_row.0 + 1) as f64
|
||
* ScrollPixelOffset::from(layout.position_map.line_height)
|
||
- layout.position_map.scroll_pixel_position.y,
|
||
);
|
||
let bounds = Bounds::from_corners(
|
||
point(layout.gutter_hitbox.left(), start_y),
|
||
point(layout.gutter_hitbox.left() + highlight_width, end_y),
|
||
);
|
||
window.paint_quad(fill(bounds, *color).corner_radii(highlight_corner_radii));
|
||
}
|
||
});
|
||
}
|
||
|
||
fn paint_blamed_display_rows(
|
||
&self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
|
||
return;
|
||
};
|
||
|
||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||
for mut blame_element in blamed_display_rows.into_iter() {
|
||
blame_element.paint(window, cx);
|
||
}
|
||
})
|
||
}
|
||
|
||
fn paint_text(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||
window.with_content_mask(
|
||
Some(ContentMask {
|
||
bounds: layout.position_map.text_hitbox.bounds,
|
||
}),
|
||
|window| {
|
||
let editor = self.editor.read(cx);
|
||
if let SelectionDragState::ReadyToDrag {
|
||
mouse_down_time, ..
|
||
} = &editor.selection_drag_state
|
||
{
|
||
let drag_and_drop_delay = Duration::from_millis(
|
||
EditorSettings::get_global(cx)
|
||
.drag_and_drop_selection
|
||
.delay
|
||
.0,
|
||
);
|
||
if mouse_down_time.elapsed() >= drag_and_drop_delay {
|
||
window.set_cursor_style(
|
||
CursorStyle::DragCopy,
|
||
&layout.position_map.text_hitbox,
|
||
);
|
||
}
|
||
} else if matches!(
|
||
editor.selection_drag_state,
|
||
SelectionDragState::Dragging { .. }
|
||
) {
|
||
window
|
||
.set_cursor_style(CursorStyle::DragCopy, &layout.position_map.text_hitbox);
|
||
} else if editor
|
||
.hovered_link_state
|
||
.as_ref()
|
||
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
|
||
{
|
||
window.set_cursor_style(
|
||
CursorStyle::PointingHand,
|
||
&layout.position_map.text_hitbox,
|
||
);
|
||
} else {
|
||
window.set_cursor_style(CursorStyle::IBeam, &layout.position_map.text_hitbox);
|
||
};
|
||
|
||
self.paint_lines_background(layout, window, cx);
|
||
let invisible_display_ranges = self.paint_highlights(layout, window, cx);
|
||
self.paint_document_colors(layout, window);
|
||
self.paint_lines(&invisible_display_ranges, layout, window, cx);
|
||
self.paint_redactions(layout, window);
|
||
self.paint_navigation_overlays(layout, window, cx);
|
||
self.paint_cursors(layout, window, cx);
|
||
self.paint_inline_diagnostics(layout, window, cx);
|
||
self.paint_inline_blame(layout, window, cx);
|
||
self.paint_inline_code_actions(layout, window, cx);
|
||
self.paint_diff_hunk_controls(layout, window, cx);
|
||
window.with_element_namespace("crease_trailers", |window| {
|
||
for trailer in layout.crease_trailers.iter_mut().flatten() {
|
||
trailer.element.paint(window, cx);
|
||
}
|
||
});
|
||
},
|
||
)
|
||
}
|
||
|
||
fn paint_highlights(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> SmallVec<[Range<DisplayPoint>; 32]> {
|
||
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
|
||
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
|
||
let line_end_overshoot = 0.15 * layout.position_map.line_height;
|
||
for (range, color) in &layout.highlighted_ranges {
|
||
self.paint_highlighted_range(
|
||
range.clone(),
|
||
true,
|
||
*color,
|
||
Pixels::ZERO,
|
||
line_end_overshoot,
|
||
layout,
|
||
window,
|
||
);
|
||
}
|
||
|
||
let corner_radius = if EditorSettings::get_global(cx).rounded_selection {
|
||
0.15 * layout.position_map.line_height
|
||
} else {
|
||
Pixels::ZERO
|
||
};
|
||
|
||
for (player_color, selections) in &layout.selections {
|
||
for selection in selections.iter() {
|
||
self.paint_highlighted_range(
|
||
selection.range.clone(),
|
||
true,
|
||
player_color.selection,
|
||
corner_radius,
|
||
corner_radius * 2.,
|
||
layout,
|
||
window,
|
||
);
|
||
|
||
if selection.is_local && !selection.range.is_empty() {
|
||
invisible_display_ranges.push(selection.range.clone());
|
||
}
|
||
}
|
||
}
|
||
invisible_display_ranges
|
||
})
|
||
}
|
||
|
||
fn paint_lines(
|
||
&mut self,
|
||
invisible_display_ranges: &[Range<DisplayPoint>],
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let whitespace_setting = self
|
||
.editor
|
||
.read(cx)
|
||
.buffer
|
||
.read(cx)
|
||
.language_settings(cx)
|
||
.show_whitespaces;
|
||
|
||
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
|
||
let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
|
||
line_with_invisibles.draw(
|
||
layout,
|
||
row,
|
||
layout.content_origin,
|
||
whitespace_setting,
|
||
invisible_display_ranges,
|
||
window,
|
||
cx,
|
||
)
|
||
}
|
||
|
||
for line_element in &mut layout.line_elements {
|
||
line_element.paint(window, cx);
|
||
}
|
||
}
|
||
|
||
fn paint_lines_background(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
|
||
let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
|
||
line_with_invisibles.draw_background(layout, row, layout.content_origin, window, cx);
|
||
}
|
||
}
|
||
|
||
fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
|
||
if layout.redacted_ranges.is_empty() {
|
||
return;
|
||
}
|
||
|
||
let line_end_overshoot = layout.line_end_overshoot();
|
||
|
||
// A softer than perfect black
|
||
let redaction_color = gpui::rgb(0x0e1111);
|
||
|
||
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
|
||
for range in layout.redacted_ranges.iter() {
|
||
self.paint_highlighted_range(
|
||
range.clone(),
|
||
true,
|
||
redaction_color.into(),
|
||
Pixels::ZERO,
|
||
line_end_overshoot,
|
||
layout,
|
||
window,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
fn paint_navigation_overlays(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
window.with_element_namespace("navigation_overlays", |window| {
|
||
for command in &mut layout.navigation_overlay_paint_commands {
|
||
let NavigationOverlayPaintCommand::Label(label) = command;
|
||
label.element.paint(window, cx);
|
||
}
|
||
});
|
||
}
|
||
|
||
fn paint_document_colors(&self, layout: &mut EditorLayout, window: &mut Window) {
|
||
let Some((colors_render_mode, image_colors)) = &layout.document_colors else {
|
||
return;
|
||
};
|
||
if image_colors.is_empty()
|
||
|| colors_render_mode == &DocumentColorsRenderMode::None
|
||
|| colors_render_mode == &DocumentColorsRenderMode::Inlay
|
||
{
|
||
return;
|
||
}
|
||
|
||
let line_end_overshoot = layout.line_end_overshoot();
|
||
|
||
for (range, color) in image_colors {
|
||
match colors_render_mode {
|
||
DocumentColorsRenderMode::Inlay | DocumentColorsRenderMode::None => return,
|
||
DocumentColorsRenderMode::Background => {
|
||
self.paint_highlighted_range(
|
||
range.clone(),
|
||
true,
|
||
*color,
|
||
Pixels::ZERO,
|
||
line_end_overshoot,
|
||
layout,
|
||
window,
|
||
);
|
||
}
|
||
DocumentColorsRenderMode::Border => {
|
||
self.paint_highlighted_range(
|
||
range.clone(),
|
||
false,
|
||
*color,
|
||
Pixels::ZERO,
|
||
line_end_overshoot,
|
||
layout,
|
||
window,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn paint_cursors(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||
for cursor in &mut layout.visible_cursors {
|
||
cursor.paint(layout.content_origin, window, cx);
|
||
}
|
||
}
|
||
|
||
fn paint_scrollbars(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||
let Some(scrollbars_layout) = layout.scrollbars_layout.take() else {
|
||
return;
|
||
};
|
||
let any_scrollbar_dragged = self.editor.read(cx).scroll_manager.any_scrollbar_dragged();
|
||
|
||
for (scrollbar_layout, axis) in scrollbars_layout.iter_scrollbars() {
|
||
let hitbox = &scrollbar_layout.hitbox;
|
||
if scrollbars_layout.visible {
|
||
let scrollbar_edges = match axis {
|
||
ScrollbarAxis::Horizontal => Edges {
|
||
top: Pixels::ZERO,
|
||
right: Pixels::ZERO,
|
||
bottom: Pixels::ZERO,
|
||
left: Pixels::ZERO,
|
||
},
|
||
ScrollbarAxis::Vertical => Edges {
|
||
top: Pixels::ZERO,
|
||
right: Pixels::ZERO,
|
||
bottom: Pixels::ZERO,
|
||
left: ScrollbarLayout::BORDER_WIDTH,
|
||
},
|
||
};
|
||
|
||
window.paint_layer(hitbox.bounds, |window| {
|
||
window.paint_quad(quad(
|
||
hitbox.bounds,
|
||
Corners::default(),
|
||
cx.theme().colors().scrollbar_track_background,
|
||
scrollbar_edges,
|
||
cx.theme().colors().scrollbar_track_border,
|
||
BorderStyle::Solid,
|
||
));
|
||
|
||
if axis == ScrollbarAxis::Vertical {
|
||
let fast_markers =
|
||
self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx);
|
||
// Refresh slow scrollbar markers in the background. Below, we
|
||
// paint whatever markers have already been computed.
|
||
self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, window, cx);
|
||
|
||
let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone();
|
||
for marker in markers.iter().chain(&fast_markers) {
|
||
let mut marker = marker.clone();
|
||
marker.bounds.origin += hitbox.origin;
|
||
window.paint_quad(marker);
|
||
}
|
||
}
|
||
|
||
if let Some(thumb_bounds) = scrollbar_layout.thumb_bounds {
|
||
let scrollbar_thumb_color = match scrollbar_layout.thumb_state {
|
||
ScrollbarThumbState::Dragging => {
|
||
cx.theme().colors().scrollbar_thumb_active_background
|
||
}
|
||
ScrollbarThumbState::Hovered => {
|
||
cx.theme().colors().scrollbar_thumb_hover_background
|
||
}
|
||
ScrollbarThumbState::Idle => {
|
||
cx.theme().colors().scrollbar_thumb_background
|
||
}
|
||
};
|
||
window.paint_quad(quad(
|
||
thumb_bounds,
|
||
Corners::default(),
|
||
scrollbar_thumb_color,
|
||
scrollbar_edges,
|
||
cx.theme().colors().scrollbar_thumb_border,
|
||
BorderStyle::Solid,
|
||
));
|
||
|
||
if any_scrollbar_dragged {
|
||
window.set_window_cursor_style(CursorStyle::Arrow);
|
||
} else {
|
||
window.set_cursor_style(CursorStyle::Arrow, hitbox);
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
window.on_mouse_event({
|
||
let editor = self.editor.clone();
|
||
let scrollbars_layout = scrollbars_layout.clone();
|
||
|
||
let mut mouse_position = window.mouse_position();
|
||
move |event: &MouseMoveEvent, phase, window, cx| {
|
||
if phase == DispatchPhase::Capture {
|
||
return;
|
||
}
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
if let Some((scrollbar_layout, axis)) = event
|
||
.pressed_button
|
||
.filter(|button| *button == MouseButton::Left)
|
||
.and(editor.scroll_manager.dragging_scrollbar_axis())
|
||
.and_then(|axis| {
|
||
scrollbars_layout
|
||
.iter_scrollbars()
|
||
.find(|(_, a)| *a == axis)
|
||
})
|
||
{
|
||
let ScrollbarLayout {
|
||
hitbox,
|
||
text_unit_size,
|
||
..
|
||
} = scrollbar_layout;
|
||
|
||
let old_position = mouse_position.along(axis);
|
||
let new_position = event.position.along(axis);
|
||
if (hitbox.origin.along(axis)..hitbox.bottom_right().along(axis))
|
||
.contains(&old_position)
|
||
{
|
||
let position = editor.scroll_position(cx).apply_along(axis, |p| {
|
||
(p + ScrollOffset::from(
|
||
(new_position - old_position) / *text_unit_size,
|
||
))
|
||
.max(0.)
|
||
});
|
||
editor.set_scroll_position(position, window, cx);
|
||
}
|
||
|
||
editor.scroll_manager.show_scrollbars(window, cx);
|
||
cx.stop_propagation();
|
||
} else if let Some((layout, axis)) = scrollbars_layout
|
||
.get_hovered_axis(window)
|
||
.filter(|_| !event.dragging())
|
||
{
|
||
if layout.thumb_hovered(&event.position) {
|
||
editor
|
||
.scroll_manager
|
||
.set_hovered_scroll_thumb_axis(axis, cx);
|
||
} else {
|
||
editor.scroll_manager.reset_scrollbar_state(cx);
|
||
}
|
||
|
||
editor.scroll_manager.show_scrollbars(window, cx);
|
||
} else {
|
||
editor.scroll_manager.reset_scrollbar_state(cx);
|
||
}
|
||
|
||
mouse_position = event.position;
|
||
})
|
||
}
|
||
});
|
||
|
||
if any_scrollbar_dragged {
|
||
window.on_mouse_event({
|
||
let editor = self.editor.clone();
|
||
move |_: &MouseUpEvent, phase, window, cx| {
|
||
if phase == DispatchPhase::Capture {
|
||
return;
|
||
}
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
if let Some((_, axis)) = scrollbars_layout.get_hovered_axis(window) {
|
||
editor
|
||
.scroll_manager
|
||
.set_hovered_scroll_thumb_axis(axis, cx);
|
||
} else {
|
||
editor.scroll_manager.reset_scrollbar_state(cx);
|
||
}
|
||
cx.stop_propagation();
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
window.on_mouse_event({
|
||
let editor = self.editor.clone();
|
||
|
||
move |event: &MouseDownEvent, phase, window, cx| {
|
||
if phase == DispatchPhase::Capture {
|
||
return;
|
||
}
|
||
let Some((scrollbar_layout, axis)) = scrollbars_layout.get_hovered_axis(window)
|
||
else {
|
||
return;
|
||
};
|
||
|
||
let ScrollbarLayout {
|
||
hitbox,
|
||
visible_range,
|
||
text_unit_size,
|
||
thumb_bounds,
|
||
..
|
||
} = scrollbar_layout;
|
||
|
||
let Some(thumb_bounds) = thumb_bounds else {
|
||
return;
|
||
};
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
editor
|
||
.scroll_manager
|
||
.set_dragged_scroll_thumb_axis(axis, cx);
|
||
|
||
let event_position = event.position.along(axis);
|
||
|
||
if event_position < thumb_bounds.origin.along(axis)
|
||
|| thumb_bounds.bottom_right().along(axis) < event_position
|
||
{
|
||
let center_position = ((event_position - hitbox.origin.along(axis))
|
||
/ *text_unit_size)
|
||
.round() as u32;
|
||
let start_position = center_position.saturating_sub(
|
||
(visible_range.end - visible_range.start) as u32 / 2,
|
||
);
|
||
|
||
let position = editor
|
||
.scroll_position(cx)
|
||
.apply_along(axis, |_| start_position as ScrollOffset);
|
||
|
||
editor.set_scroll_position(position, window, cx);
|
||
} else {
|
||
editor.scroll_manager.show_scrollbars(window, cx);
|
||
}
|
||
|
||
cx.stop_propagation();
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
fn collect_fast_scrollbar_markers(
|
||
&self,
|
||
layout: &EditorLayout,
|
||
scrollbar_layout: &ScrollbarLayout,
|
||
cx: &mut App,
|
||
) -> Vec<PaintQuad> {
|
||
const LIMIT: usize = 100;
|
||
if !EditorSettings::get_global(cx).scrollbar.cursors || layout.cursors.len() > LIMIT {
|
||
return vec![];
|
||
}
|
||
let cursor_ranges = layout
|
||
.cursors
|
||
.iter()
|
||
.map(|(point, color)| ColoredRange {
|
||
start: point.row(),
|
||
end: point.row(),
|
||
color: *color,
|
||
})
|
||
.collect_vec();
|
||
scrollbar_layout.marker_quads_for_ranges(cursor_ranges, None)
|
||
}
|
||
|
||
fn refresh_slow_scrollbar_markers(
|
||
&self,
|
||
layout: &EditorLayout,
|
||
scrollbar_layout: &ScrollbarLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
self.editor.update(cx, |editor, cx| {
|
||
if editor.buffer_kind(cx) != ItemBufferKind::Singleton
|
||
|| !editor
|
||
.scrollbar_marker_state
|
||
.should_refresh(scrollbar_layout.hitbox.size)
|
||
{
|
||
return;
|
||
}
|
||
|
||
let scrollbar_layout = scrollbar_layout.clone();
|
||
let background_highlights = editor.background_highlights.clone();
|
||
let snapshot = layout.position_map.snapshot.clone();
|
||
let theme = cx.theme().clone();
|
||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||
|
||
editor.scrollbar_marker_state.dirty = false;
|
||
editor.scrollbar_marker_state.pending_refresh =
|
||
Some(cx.spawn_in(window, async move |editor, cx| {
|
||
let scrollbar_size = scrollbar_layout.hitbox.size;
|
||
let scrollbar_markers = cx
|
||
.background_spawn(async move {
|
||
let max_point = snapshot.display_snapshot.buffer_snapshot().max_point();
|
||
let mut marker_quads = Vec::new();
|
||
if scrollbar_settings.git_diff {
|
||
let marker_row_ranges =
|
||
snapshot.buffer_snapshot().diff_hunks().map(|hunk| {
|
||
let start_display_row =
|
||
MultiBufferPoint::new(hunk.row_range.start.0, 0)
|
||
.to_display_point(&snapshot.display_snapshot)
|
||
.row();
|
||
let mut end_display_row =
|
||
MultiBufferPoint::new(hunk.row_range.end.0, 0)
|
||
.to_display_point(&snapshot.display_snapshot)
|
||
.row();
|
||
if end_display_row != start_display_row {
|
||
end_display_row.0 -= 1;
|
||
}
|
||
let color = match &hunk.status().kind {
|
||
DiffHunkStatusKind::Added => {
|
||
theme.colors().version_control_added
|
||
}
|
||
DiffHunkStatusKind::Modified => {
|
||
theme.colors().version_control_modified
|
||
}
|
||
DiffHunkStatusKind::Deleted => {
|
||
theme.colors().version_control_deleted
|
||
}
|
||
};
|
||
ColoredRange {
|
||
start: start_display_row,
|
||
end: end_display_row,
|
||
color,
|
||
}
|
||
});
|
||
|
||
marker_quads.extend(
|
||
scrollbar_layout
|
||
.marker_quads_for_ranges(marker_row_ranges, Some(0)),
|
||
);
|
||
}
|
||
|
||
for (background_highlight_id, (_, background_ranges)) in
|
||
background_highlights.iter()
|
||
{
|
||
let is_search_highlights = *background_highlight_id
|
||
== HighlightKey::BufferSearchHighlights;
|
||
let is_text_highlights =
|
||
*background_highlight_id == HighlightKey::SelectedTextHighlight;
|
||
let is_symbol_occurrences = *background_highlight_id
|
||
== HighlightKey::DocumentHighlightRead
|
||
|| *background_highlight_id
|
||
== HighlightKey::DocumentHighlightWrite;
|
||
if (is_search_highlights && scrollbar_settings.search_results)
|
||
|| (is_text_highlights && scrollbar_settings.selected_text)
|
||
|| (is_symbol_occurrences && scrollbar_settings.selected_symbol)
|
||
{
|
||
let mut color = theme.status().info;
|
||
if is_symbol_occurrences {
|
||
color.fade_out(0.5);
|
||
}
|
||
let marker_row_ranges = background_ranges.iter().map(|range| {
|
||
let display_start = range
|
||
.start
|
||
.to_display_point(&snapshot.display_snapshot);
|
||
let display_end =
|
||
range.end.to_display_point(&snapshot.display_snapshot);
|
||
ColoredRange {
|
||
start: display_start.row(),
|
||
end: display_end.row(),
|
||
color,
|
||
}
|
||
});
|
||
marker_quads.extend(
|
||
scrollbar_layout
|
||
.marker_quads_for_ranges(marker_row_ranges, Some(1)),
|
||
);
|
||
}
|
||
}
|
||
|
||
if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
|
||
let diagnostics = snapshot
|
||
.buffer_snapshot()
|
||
.diagnostics_in_range::<Point>(Point::zero()..max_point)
|
||
// Don't show diagnostics the user doesn't care about
|
||
.filter(|diagnostic| {
|
||
match (
|
||
scrollbar_settings.diagnostics,
|
||
diagnostic.diagnostic.severity,
|
||
) {
|
||
(ScrollbarDiagnostics::All, _) => true,
|
||
(
|
||
ScrollbarDiagnostics::Error,
|
||
lsp::DiagnosticSeverity::ERROR,
|
||
) => true,
|
||
(
|
||
ScrollbarDiagnostics::Warning,
|
||
lsp::DiagnosticSeverity::ERROR
|
||
| lsp::DiagnosticSeverity::WARNING,
|
||
) => true,
|
||
(
|
||
ScrollbarDiagnostics::Information,
|
||
lsp::DiagnosticSeverity::ERROR
|
||
| lsp::DiagnosticSeverity::WARNING
|
||
| lsp::DiagnosticSeverity::INFORMATION,
|
||
) => true,
|
||
(_, _) => false,
|
||
}
|
||
})
|
||
// We want to sort by severity, in order to paint the most severe diagnostics last.
|
||
.sorted_by_key(|diagnostic| {
|
||
std::cmp::Reverse(diagnostic.diagnostic.severity)
|
||
});
|
||
|
||
let marker_row_ranges = diagnostics.into_iter().map(|diagnostic| {
|
||
let start_display = diagnostic
|
||
.range
|
||
.start
|
||
.to_display_point(&snapshot.display_snapshot);
|
||
let end_display = diagnostic
|
||
.range
|
||
.end
|
||
.to_display_point(&snapshot.display_snapshot);
|
||
let color = match diagnostic.diagnostic.severity {
|
||
lsp::DiagnosticSeverity::ERROR => theme.status().error,
|
||
lsp::DiagnosticSeverity::WARNING => theme.status().warning,
|
||
lsp::DiagnosticSeverity::INFORMATION => theme.status().info,
|
||
_ => theme.status().hint,
|
||
};
|
||
ColoredRange {
|
||
start: start_display.row(),
|
||
end: end_display.row(),
|
||
color,
|
||
}
|
||
});
|
||
marker_quads.extend(
|
||
scrollbar_layout
|
||
.marker_quads_for_ranges(marker_row_ranges, Some(2)),
|
||
);
|
||
}
|
||
|
||
Arc::from(marker_quads)
|
||
})
|
||
.await;
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
editor.scrollbar_marker_state.markers = scrollbar_markers;
|
||
editor.scrollbar_marker_state.scrollbar_size = scrollbar_size;
|
||
editor.scrollbar_marker_state.pending_refresh = None;
|
||
cx.notify();
|
||
})?;
|
||
|
||
Ok(())
|
||
}));
|
||
});
|
||
}
|
||
|
||
fn paint_highlighted_range(
|
||
&self,
|
||
range: Range<DisplayPoint>,
|
||
fill: bool,
|
||
color: Hsla,
|
||
corner_radius: Pixels,
|
||
line_end_overshoot: Pixels,
|
||
layout: &EditorLayout,
|
||
window: &mut Window,
|
||
) {
|
||
let start_row = layout.visible_display_row_range.start;
|
||
let end_row = layout.visible_display_row_range.end;
|
||
if range.start != range.end {
|
||
let row_range = if range.end.column() == 0 {
|
||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
|
||
} else {
|
||
cmp::max(range.start.row(), start_row)
|
||
..cmp::min(range.end.row().next_row(), end_row)
|
||
};
|
||
|
||
let highlighted_range = HighlightedRange {
|
||
color,
|
||
line_height: layout.position_map.line_height,
|
||
corner_radius,
|
||
start_y: layout.content_origin.y
|
||
+ Pixels::from(
|
||
(row_range.start.as_f64() - layout.position_map.scroll_position.y)
|
||
* ScrollOffset::from(layout.position_map.line_height),
|
||
),
|
||
lines: row_range
|
||
.iter_rows()
|
||
.map(|row| {
|
||
let line_layout =
|
||
&layout.position_map.line_layouts[row.minus(start_row) as usize];
|
||
let alignment_offset =
|
||
line_layout.alignment_offset(layout.text_align, layout.content_width);
|
||
HighlightedRangeLine {
|
||
start_x: if row == range.start.row() {
|
||
layout.content_origin.x
|
||
+ Pixels::from(
|
||
ScrollPixelOffset::from(
|
||
line_layout.x_for_index(range.start.column() as usize)
|
||
+ alignment_offset,
|
||
) - layout.position_map.scroll_pixel_position.x,
|
||
)
|
||
} else {
|
||
layout.content_origin.x + alignment_offset
|
||
- Pixels::from(layout.position_map.scroll_pixel_position.x)
|
||
},
|
||
end_x: if row == range.end.row() {
|
||
layout.content_origin.x
|
||
+ Pixels::from(
|
||
ScrollPixelOffset::from(
|
||
line_layout.x_for_index(range.end.column() as usize)
|
||
+ alignment_offset,
|
||
) - layout.position_map.scroll_pixel_position.x,
|
||
)
|
||
} else {
|
||
Pixels::from(
|
||
ScrollPixelOffset::from(
|
||
layout.content_origin.x
|
||
+ line_layout.width
|
||
+ alignment_offset
|
||
+ line_end_overshoot,
|
||
) - layout.position_map.scroll_pixel_position.x,
|
||
)
|
||
},
|
||
}
|
||
})
|
||
.collect(),
|
||
};
|
||
|
||
highlighted_range.paint(fill, layout.position_map.text_hitbox.bounds, window);
|
||
}
|
||
}
|
||
|
||
fn paint_inline_diagnostics(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for mut inline_diagnostic in layout.inline_diagnostics.drain() {
|
||
inline_diagnostic.1.paint(window, cx);
|
||
}
|
||
}
|
||
|
||
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||
if let Some(mut blame_layout) = layout.inline_blame_layout.take() {
|
||
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
|
||
blame_layout.element.paint(window, cx);
|
||
})
|
||
}
|
||
}
|
||
|
||
fn paint_inline_code_actions(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
if let Some(mut inline_code_actions) = layout.inline_code_actions.take() {
|
||
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
|
||
inline_code_actions.paint(window, cx);
|
||
})
|
||
}
|
||
}
|
||
|
||
fn paint_diff_hunk_controls(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for mut diff_hunk_control in layout.diff_hunk_controls.drain(..) {
|
||
diff_hunk_control.paint(window, cx);
|
||
}
|
||
}
|
||
|
||
fn paint_minimap(&self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||
if let Some(mut layout) = layout.minimap.take() {
|
||
let minimap_hitbox = layout.thumb_layout.hitbox.clone();
|
||
let dragging_minimap = self.editor.read(cx).scroll_manager.is_dragging_minimap();
|
||
|
||
window.paint_layer(layout.thumb_layout.hitbox.bounds, |window| {
|
||
window.with_element_namespace("minimap", |window| {
|
||
layout.minimap.paint(window, cx);
|
||
if let Some(thumb_bounds) = layout.thumb_layout.thumb_bounds {
|
||
let minimap_thumb_color = match layout.thumb_layout.thumb_state {
|
||
ScrollbarThumbState::Idle => {
|
||
cx.theme().colors().minimap_thumb_background
|
||
}
|
||
ScrollbarThumbState::Hovered => {
|
||
cx.theme().colors().minimap_thumb_hover_background
|
||
}
|
||
ScrollbarThumbState::Dragging => {
|
||
cx.theme().colors().minimap_thumb_active_background
|
||
}
|
||
};
|
||
let minimap_thumb_border = match layout.thumb_border_style {
|
||
MinimapThumbBorder::Full => Edges::all(ScrollbarLayout::BORDER_WIDTH),
|
||
MinimapThumbBorder::LeftOnly => Edges {
|
||
left: ScrollbarLayout::BORDER_WIDTH,
|
||
..Default::default()
|
||
},
|
||
MinimapThumbBorder::LeftOpen => Edges {
|
||
right: ScrollbarLayout::BORDER_WIDTH,
|
||
top: ScrollbarLayout::BORDER_WIDTH,
|
||
bottom: ScrollbarLayout::BORDER_WIDTH,
|
||
..Default::default()
|
||
},
|
||
MinimapThumbBorder::RightOpen => Edges {
|
||
left: ScrollbarLayout::BORDER_WIDTH,
|
||
top: ScrollbarLayout::BORDER_WIDTH,
|
||
bottom: ScrollbarLayout::BORDER_WIDTH,
|
||
..Default::default()
|
||
},
|
||
MinimapThumbBorder::None => Default::default(),
|
||
};
|
||
|
||
window.paint_layer(minimap_hitbox.bounds, |window| {
|
||
window.paint_quad(quad(
|
||
thumb_bounds,
|
||
Corners::default(),
|
||
minimap_thumb_color,
|
||
minimap_thumb_border,
|
||
cx.theme().colors().minimap_thumb_border,
|
||
BorderStyle::Solid,
|
||
));
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
if dragging_minimap {
|
||
window.set_window_cursor_style(CursorStyle::Arrow);
|
||
} else {
|
||
window.set_cursor_style(CursorStyle::Arrow, &minimap_hitbox);
|
||
}
|
||
|
||
let minimap_axis = ScrollbarAxis::Vertical;
|
||
let pixels_per_line = Pixels::from(
|
||
ScrollPixelOffset::from(minimap_hitbox.size.height) / layout.max_scroll_top,
|
||
)
|
||
.min(layout.minimap_line_height);
|
||
|
||
let mut mouse_position = window.mouse_position();
|
||
|
||
window.on_mouse_event({
|
||
let editor = self.editor.clone();
|
||
|
||
let minimap_hitbox = minimap_hitbox.clone();
|
||
|
||
move |event: &MouseMoveEvent, phase, window, cx| {
|
||
if phase == DispatchPhase::Capture {
|
||
return;
|
||
}
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
if event.pressed_button == Some(MouseButton::Left)
|
||
&& editor.scroll_manager.is_dragging_minimap()
|
||
{
|
||
let old_position = mouse_position.along(minimap_axis);
|
||
let new_position = event.position.along(minimap_axis);
|
||
if (minimap_hitbox.origin.along(minimap_axis)
|
||
..minimap_hitbox.bottom_right().along(minimap_axis))
|
||
.contains(&old_position)
|
||
{
|
||
let position =
|
||
editor.scroll_position(cx).apply_along(minimap_axis, |p| {
|
||
(p + ScrollPixelOffset::from(
|
||
(new_position - old_position) / pixels_per_line,
|
||
))
|
||
.max(0.)
|
||
});
|
||
|
||
editor.set_scroll_position(position, window, cx);
|
||
}
|
||
cx.stop_propagation();
|
||
} else if minimap_hitbox.is_hovered(window) {
|
||
editor.scroll_manager.set_is_hovering_minimap_thumb(
|
||
!event.dragging()
|
||
&& layout
|
||
.thumb_layout
|
||
.thumb_bounds
|
||
.is_some_and(|bounds| bounds.contains(&event.position)),
|
||
cx,
|
||
);
|
||
|
||
// Stop hover events from propagating to the
|
||
// underlying editor if the minimap hitbox is hovered
|
||
if !event.dragging() {
|
||
cx.stop_propagation();
|
||
}
|
||
} else {
|
||
editor.scroll_manager.hide_minimap_thumb(cx);
|
||
}
|
||
mouse_position = event.position;
|
||
});
|
||
}
|
||
});
|
||
|
||
if dragging_minimap {
|
||
window.on_mouse_event({
|
||
let editor = self.editor.clone();
|
||
move |event: &MouseUpEvent, phase, window, cx| {
|
||
if phase == DispatchPhase::Capture {
|
||
return;
|
||
}
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
if minimap_hitbox.is_hovered(window) {
|
||
editor.scroll_manager.set_is_hovering_minimap_thumb(
|
||
layout
|
||
.thumb_layout
|
||
.thumb_bounds
|
||
.is_some_and(|bounds| bounds.contains(&event.position)),
|
||
cx,
|
||
);
|
||
} else {
|
||
editor.scroll_manager.hide_minimap_thumb(cx);
|
||
}
|
||
cx.stop_propagation();
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
window.on_mouse_event({
|
||
let editor = self.editor.clone();
|
||
|
||
move |event: &MouseDownEvent, phase, window, cx| {
|
||
if phase == DispatchPhase::Capture || !minimap_hitbox.is_hovered(window) {
|
||
return;
|
||
}
|
||
|
||
let event_position = event.position;
|
||
|
||
let Some(thumb_bounds) = layout.thumb_layout.thumb_bounds else {
|
||
return;
|
||
};
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
if !thumb_bounds.contains(&event_position) {
|
||
let click_position =
|
||
event_position.relative_to(&minimap_hitbox.origin).y;
|
||
|
||
let top_position = (click_position
|
||
- thumb_bounds.size.along(minimap_axis) / 2.0)
|
||
.max(Pixels::ZERO);
|
||
|
||
let scroll_offset = (layout.minimap_scroll_top
|
||
+ ScrollPixelOffset::from(
|
||
top_position / layout.minimap_line_height,
|
||
))
|
||
.min(layout.max_scroll_top);
|
||
|
||
let scroll_position = editor
|
||
.scroll_position(cx)
|
||
.apply_along(minimap_axis, |_| scroll_offset);
|
||
editor.set_scroll_position(scroll_position, window, cx);
|
||
}
|
||
|
||
editor.scroll_manager.set_is_dragging_minimap(cx);
|
||
cx.stop_propagation();
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
fn paint_spacer_blocks(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for mut block in layout.spacer_blocks.drain(..) {
|
||
let mut bounds = layout.hitbox.bounds;
|
||
bounds.origin.x += layout.gutter_hitbox.bounds.size.width;
|
||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||
block.element.paint(window, cx);
|
||
})
|
||
}
|
||
}
|
||
|
||
fn paint_non_spacer_blocks(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
for mut block in layout.blocks.drain(..) {
|
||
if block.overlaps_gutter {
|
||
block.element.paint(window, cx);
|
||
} else {
|
||
let mut bounds = layout.hitbox.bounds;
|
||
bounds.origin.x += layout.gutter_hitbox.bounds.size.width;
|
||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||
block.element.paint(window, cx);
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
fn paint_edit_prediction_popover(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
if let Some(edit_prediction_popover) = layout.edit_prediction_popover.as_mut() {
|
||
edit_prediction_popover.paint(window, cx);
|
||
}
|
||
}
|
||
|
||
fn paint_mouse_context_menu(
|
||
&mut self,
|
||
layout: &mut EditorLayout,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
|
||
mouse_context_menu.paint(window, cx);
|
||
}
|
||
}
|
||
|
||
fn shape_line_number(
|
||
&self,
|
||
text: SharedString,
|
||
color: Hsla,
|
||
window: &mut Window,
|
||
) -> ShapedLine {
|
||
let run = TextRun {
|
||
len: text.len(),
|
||
font: self.style.text.font(),
|
||
color,
|
||
..Default::default()
|
||
};
|
||
window.text_system().shape_line(
|
||
text,
|
||
self.style.text.font_size.to_pixels(window.rem_size()),
|
||
&[run],
|
||
None,
|
||
)
|
||
}
|
||
|
||
fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
|
||
let unstaged = status.has_secondary_hunk();
|
||
let unstaged_hollow = matches!(
|
||
ProjectSettings::get_global(cx).git.hunk_style,
|
||
GitHunkStyleSetting::UnstagedHollow
|
||
);
|
||
|
||
unstaged == unstaged_hollow
|
||
}
|
||
|
||
#[cfg(debug_assertions)]
|
||
fn layout_debug_ranges(
|
||
selections: &mut Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||
anchor_range: Range<Anchor>,
|
||
display_snapshot: &DisplaySnapshot,
|
||
cx: &App,
|
||
) {
|
||
let theme = cx.theme();
|
||
text::debug::GlobalDebugRanges::with_locked(|debug_ranges| {
|
||
if debug_ranges.ranges.is_empty() {
|
||
return;
|
||
}
|
||
let buffer_snapshot = &display_snapshot.buffer_snapshot();
|
||
for (excerpt_buffer_snapshot, buffer_range, _) in
|
||
buffer_snapshot.range_to_buffer_ranges(anchor_range.start..anchor_range.end)
|
||
{
|
||
let buffer_range = excerpt_buffer_snapshot.anchor_after(buffer_range.start)
|
||
..excerpt_buffer_snapshot.anchor_before(buffer_range.end);
|
||
selections.extend(debug_ranges.ranges.iter().flat_map(|debug_range| {
|
||
debug_range.ranges.iter().filter_map(|range| {
|
||
let player_color = theme
|
||
.players()
|
||
.color_for_participant(debug_range.occurrence_index as u32 + 1);
|
||
if range.start.buffer_id != excerpt_buffer_snapshot.remote_id() {
|
||
return None;
|
||
}
|
||
let clipped_start = range
|
||
.start
|
||
.max(&buffer_range.start, &excerpt_buffer_snapshot);
|
||
let clipped_end =
|
||
range.end.min(&buffer_range.end, &excerpt_buffer_snapshot);
|
||
let range = buffer_snapshot
|
||
.buffer_anchor_range_to_anchor_range(*clipped_start..*clipped_end)?;
|
||
let start = range.start.to_display_point(display_snapshot);
|
||
let end = range.end.to_display_point(display_snapshot);
|
||
let selection_layout = SelectionLayout {
|
||
head: start,
|
||
range: start..end,
|
||
cursor_shape: CursorShape::Bar,
|
||
is_newest: false,
|
||
is_local: false,
|
||
active_rows: start.row()..end.row(),
|
||
user_name: Some(SharedString::new(debug_range.value.clone())),
|
||
};
|
||
Some((player_color, vec![selection_layout]))
|
||
})
|
||
}));
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
struct Gutter<'a> {
|
||
line_height: Pixels,
|
||
range: Range<DisplayRow>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
dimensions: &'a GutterDimensions,
|
||
hitbox: &'a Hitbox,
|
||
snapshot: &'a EditorSnapshot,
|
||
row_infos: &'a [RowInfo],
|
||
}
|
||
|
||
impl Gutter<'_> {
|
||
fn layout_item_skipping_folds(
|
||
&self,
|
||
display_row: DisplayRow,
|
||
render_item: impl Fn(&mut Context<'_, Editor>, &mut Window) -> AnyElement,
|
||
window: &mut Window,
|
||
cx: &mut Context<'_, Editor>,
|
||
) -> Option<AnyElement> {
|
||
let row = MultiBufferRow(
|
||
DisplayPoint::new(display_row, 0)
|
||
.to_point(self.snapshot)
|
||
.row,
|
||
);
|
||
if self.snapshot.is_line_folded(row) {
|
||
return None;
|
||
}
|
||
|
||
self.layout_item(display_row, render_item, window, cx)
|
||
}
|
||
|
||
fn layout_item(
|
||
&self,
|
||
display_row: DisplayRow,
|
||
render_item: impl Fn(&mut Context<'_, Editor>, &mut Window) -> AnyElement,
|
||
window: &mut Window,
|
||
cx: &mut Context<'_, Editor>,
|
||
) -> Option<AnyElement> {
|
||
if !self.range.contains(&display_row) {
|
||
return None;
|
||
}
|
||
|
||
if self
|
||
.row_infos
|
||
.get((display_row.0.saturating_sub(self.range.start.0)) as usize)
|
||
.is_some_and(|row_info| {
|
||
row_info.expand_info.is_some()
|
||
|| row_info
|
||
.diff_status
|
||
.is_some_and(|status| status.is_deleted())
|
||
})
|
||
{
|
||
return None;
|
||
}
|
||
|
||
let button = self.prepaint_button(render_item(cx, window), display_row, window, cx);
|
||
Some(button)
|
||
}
|
||
|
||
fn prepaint_button(
|
||
&self,
|
||
mut button: AnyElement,
|
||
row: DisplayRow,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> AnyElement {
|
||
let available_space = size(
|
||
AvailableSpace::MinContent,
|
||
AvailableSpace::Definite(self.line_height),
|
||
);
|
||
let indicator_size = button.layout_as_root(available_space, window, cx);
|
||
let git_gutter_width = EditorElement::gutter_strip_width(self.line_height)
|
||
+ self.dimensions.git_blame_entries_width.unwrap_or_default();
|
||
|
||
let x = git_gutter_width + px(2.);
|
||
|
||
let mut y = Pixels::from(
|
||
(row.as_f64() - self.scroll_position.y) * ScrollPixelOffset::from(self.line_height),
|
||
);
|
||
y += (self.line_height - indicator_size.height) / 2.;
|
||
|
||
button.prepaint_as_root(
|
||
self.hitbox.origin + point(x, y),
|
||
available_space,
|
||
window,
|
||
cx,
|
||
);
|
||
button
|
||
}
|
||
}
|
||
|
||
pub fn render_breadcrumb_text(
|
||
mut segments: Vec<HighlightedText>,
|
||
breadcrumb_font: Option<Font>,
|
||
prefix: Option<gpui::AnyElement>,
|
||
active_item: &dyn ItemHandle,
|
||
multibuffer_header: bool,
|
||
window: &mut Window,
|
||
cx: &App,
|
||
) -> gpui::AnyElement {
|
||
const MAX_SEGMENTS: usize = 12;
|
||
|
||
let element = h_flex().flex_grow_1().text_ui(cx);
|
||
|
||
let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
|
||
let suffix_start_ix = cmp::max(
|
||
prefix_end_ix,
|
||
segments.len().saturating_sub(MAX_SEGMENTS / 2),
|
||
);
|
||
|
||
if suffix_start_ix > prefix_end_ix {
|
||
segments.splice(
|
||
prefix_end_ix..suffix_start_ix,
|
||
Some(HighlightedText {
|
||
text: "⋯".into(),
|
||
highlights: vec![],
|
||
}),
|
||
);
|
||
}
|
||
|
||
let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| {
|
||
let mut text_style = window.text_style();
|
||
if let Some(font) = &breadcrumb_font {
|
||
text_style.font_family = font.family.clone();
|
||
text_style.font_features = font.features.clone();
|
||
text_style.font_style = font.style;
|
||
text_style.font_weight = font.weight;
|
||
}
|
||
text_style.color = Color::Muted.color(cx);
|
||
|
||
if index == 0
|
||
&& !workspace::TabBarSettings::get_global(cx).show
|
||
&& active_item.is_dirty(cx)
|
||
&& let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
|
||
{
|
||
return styled_element;
|
||
}
|
||
|
||
StyledText::new(segment.text.replace('\n', " "))
|
||
.with_default_highlights(&text_style, segment.highlights)
|
||
.into_any()
|
||
});
|
||
|
||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||
Label::new("›").color(Color::Placeholder).into_any_element()
|
||
});
|
||
|
||
let breadcrumbs_stack = h_flex()
|
||
.gap_1()
|
||
.when(multibuffer_header, |this| {
|
||
this.pl_2()
|
||
.border_l_1()
|
||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||
})
|
||
.children(breadcrumbs);
|
||
|
||
let breadcrumbs = if let Some(prefix) = prefix {
|
||
h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
|
||
} else {
|
||
breadcrumbs_stack
|
||
};
|
||
|
||
let editor = active_item
|
||
.downcast::<Editor>()
|
||
.map(|editor| editor.downgrade());
|
||
|
||
let has_project_path = active_item.project_path(cx).is_some();
|
||
|
||
match editor {
|
||
Some(editor) => element
|
||
.id("breadcrumb_container")
|
||
.when(!multibuffer_header, |this| this.overflow_x_scroll())
|
||
.child(
|
||
ButtonLike::new("toggle outline view")
|
||
.child(breadcrumbs)
|
||
.when(multibuffer_header, |this| {
|
||
this.style(ButtonStyle::Transparent)
|
||
})
|
||
.when(!multibuffer_header, |this| {
|
||
let focus_handle = editor.upgrade().unwrap().focus_handle(&cx);
|
||
|
||
this.tooltip(Tooltip::element(move |_window, cx| {
|
||
v_flex()
|
||
.gap_1()
|
||
.child(
|
||
h_flex()
|
||
.gap_1()
|
||
.justify_between()
|
||
.child(Label::new("Show Symbol Outline"))
|
||
.child(ui::KeyBinding::for_action_in(
|
||
&zed_actions::outline::ToggleOutline,
|
||
&focus_handle,
|
||
cx,
|
||
)),
|
||
)
|
||
.when(has_project_path, |this| {
|
||
this.child(
|
||
h_flex()
|
||
.gap_1()
|
||
.justify_between()
|
||
.pt_1()
|
||
.border_t_1()
|
||
.border_color(cx.theme().colors().border_variant)
|
||
.child(Label::new("Right-Click to Copy Path")),
|
||
)
|
||
})
|
||
.into_any_element()
|
||
}))
|
||
.on_click({
|
||
let editor = editor.clone();
|
||
move |_, window, cx| {
|
||
if let Some((editor, callback)) = editor
|
||
.upgrade()
|
||
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
|
||
{
|
||
callback(editor.to_any_view(), window, cx);
|
||
}
|
||
}
|
||
})
|
||
.when(has_project_path, |this| {
|
||
this.on_right_click({
|
||
let editor = editor.clone();
|
||
move |_, _, cx| {
|
||
if let Some(abs_path) = editor.upgrade().and_then(|editor| {
|
||
editor.update(cx, |editor, cx| {
|
||
editor.target_file_abs_path(cx)
|
||
})
|
||
}) {
|
||
if let Some(path_str) = abs_path.to_str() {
|
||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||
path_str.to_string(),
|
||
));
|
||
}
|
||
}
|
||
}
|
||
})
|
||
})
|
||
}),
|
||
)
|
||
.into_any_element(),
|
||
None => element
|
||
.h(rems_from_px(22.)) // Match the height and padding of the `ButtonLike` in the other arm.
|
||
.pl_1()
|
||
.child(breadcrumbs)
|
||
.into_any_element(),
|
||
}
|
||
}
|
||
|
||
fn apply_dirty_filename_style(
|
||
segment: &HighlightedText,
|
||
text_style: &gpui::TextStyle,
|
||
cx: &App,
|
||
) -> Option<gpui::AnyElement> {
|
||
let text = segment.text.replace('\n', " ");
|
||
|
||
let filename_position = std::path::Path::new(segment.text.as_ref())
|
||
.file_name()
|
||
.and_then(|f| {
|
||
let filename_str = f.to_string_lossy();
|
||
segment.text.rfind(filename_str.as_ref())
|
||
})?;
|
||
|
||
let bold_weight = FontWeight::BOLD;
|
||
let default_color = Color::Default.color(cx);
|
||
|
||
if filename_position == 0 {
|
||
let mut filename_style = text_style.clone();
|
||
filename_style.font_weight = bold_weight;
|
||
filename_style.color = default_color;
|
||
|
||
return Some(
|
||
StyledText::new(text)
|
||
.with_default_highlights(&filename_style, [])
|
||
.into_any(),
|
||
);
|
||
}
|
||
|
||
let highlight_style = gpui::HighlightStyle {
|
||
font_weight: Some(bold_weight),
|
||
color: Some(default_color),
|
||
..Default::default()
|
||
};
|
||
|
||
let highlight = vec![(filename_position..text.len(), highlight_style)];
|
||
Some(
|
||
StyledText::new(text)
|
||
.with_default_highlights(text_style, highlight)
|
||
.into_any(),
|
||
)
|
||
}
|
||
|
||
fn render_inline_blame_entry(
|
||
blame_entry: BlameEntry,
|
||
style: &EditorStyle,
|
||
cx: &mut App,
|
||
) -> Option<AnyElement> {
|
||
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||
renderer.render_inline_blame_entry(&style.text, blame_entry, cx)
|
||
}
|
||
|
||
fn render_blame_entry_popover(
|
||
blame_entry: BlameEntry,
|
||
scroll_handle: ScrollHandle,
|
||
commit_message: Option<ParsedCommitMessage>,
|
||
markdown: Entity<Markdown>,
|
||
workspace: WeakEntity<Workspace>,
|
||
blame: &Entity<GitBlame>,
|
||
buffer: BufferId,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<AnyElement> {
|
||
if markdown.read(cx).is_parsing() {
|
||
return None;
|
||
}
|
||
|
||
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||
let blame = blame.read(cx);
|
||
let repository = blame.repository(cx, buffer)?;
|
||
renderer.render_blame_entry_popover(
|
||
blame_entry,
|
||
scroll_handle,
|
||
commit_message,
|
||
markdown,
|
||
repository,
|
||
workspace,
|
||
window,
|
||
cx,
|
||
)
|
||
}
|
||
|
||
fn render_blame_entry(
|
||
ix: usize,
|
||
blame: &Entity<GitBlame>,
|
||
blame_entry: BlameEntry,
|
||
style: &EditorStyle,
|
||
last_used_color: &mut Option<(Hsla, Oid)>,
|
||
editor: Entity<Editor>,
|
||
workspace: Entity<Workspace>,
|
||
buffer: BufferId,
|
||
renderer: &dyn BlameRenderer,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Option<AnyElement> {
|
||
let index: u32 = blame_entry.sha.into();
|
||
let mut sha_color = cx.theme().players().color_for_participant(index).cursor;
|
||
|
||
// If the last color we used is the same as the one we get for this line, but
|
||
// the commit SHAs are different, then we try again to get a different color.
|
||
if let Some((color, sha)) = *last_used_color
|
||
&& sha != blame_entry.sha
|
||
&& color == sha_color
|
||
{
|
||
sha_color = cx.theme().players().color_for_participant(index + 1).cursor;
|
||
}
|
||
last_used_color.replace((sha_color, blame_entry.sha));
|
||
|
||
let blame = blame.read(cx);
|
||
let details = blame.details_for_entry(buffer, &blame_entry);
|
||
let repository = blame.repository(cx, buffer)?;
|
||
renderer.render_blame_entry(
|
||
&style.text,
|
||
blame_entry,
|
||
details,
|
||
repository,
|
||
workspace.downgrade(),
|
||
editor,
|
||
ix,
|
||
sha_color,
|
||
window,
|
||
cx,
|
||
)
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub(crate) struct LineWithInvisibles {
|
||
fragments: SmallVec<[LineFragment; 1]>,
|
||
invisibles: Vec<Invisible>,
|
||
len: usize,
|
||
pub(crate) width: Pixels,
|
||
font_size: Pixels,
|
||
}
|
||
|
||
enum LineFragment {
|
||
Text(ShapedLine),
|
||
Element {
|
||
id: ChunkRendererId,
|
||
element: Option<AnyElement>,
|
||
size: Size<Pixels>,
|
||
len: usize,
|
||
},
|
||
}
|
||
|
||
impl fmt::Debug for LineFragment {
|
||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||
match self {
|
||
LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
|
||
LineFragment::Element { size, len, .. } => f
|
||
.debug_struct("Element")
|
||
.field("size", size)
|
||
.field("len", len)
|
||
.finish(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl LineWithInvisibles {
|
||
fn from_chunks<'a>(
|
||
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
||
editor_style: &EditorStyle,
|
||
max_line_len: usize,
|
||
max_line_count: usize,
|
||
editor_mode: &EditorMode,
|
||
text_width: Pixels,
|
||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||
bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Vec<Self> {
|
||
let text_style = &editor_style.text;
|
||
let mut layouts = Vec::with_capacity(max_line_count);
|
||
let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
|
||
let mut line = String::new();
|
||
// Byte offset into the logical line used to position invisible markers.
|
||
// Unlike `line`, this is not cleared when we flush `shape_line` for
|
||
// mid-line inlays/replacements, so marker offsets stay correct in that case.
|
||
let mut line_byte_offset: usize = 0;
|
||
let mut invisibles = Vec::new();
|
||
let mut width = Pixels::ZERO;
|
||
let mut len = 0;
|
||
let mut styles = Vec::new();
|
||
let mut non_whitespace_added = false;
|
||
let mut row = 0;
|
||
let mut line_exceeded_max_len = false;
|
||
let font_size = text_style.font_size.to_pixels(window.rem_size());
|
||
let min_contrast = EditorSettings::get_global(cx).minimum_contrast_for_highlights;
|
||
|
||
let ellipsis = SharedString::from("⋯");
|
||
|
||
for highlighted_chunk in chunks.chain([HighlightedChunk {
|
||
text: "\n",
|
||
style: None,
|
||
is_tab: false,
|
||
is_inlay: false,
|
||
replacement: None,
|
||
}]) {
|
||
if let Some(replacement) = highlighted_chunk.replacement {
|
||
if line_exceeded_max_len {
|
||
continue;
|
||
}
|
||
|
||
if len + line.len() + highlighted_chunk.text.len() > max_line_len {
|
||
line_exceeded_max_len = true;
|
||
continue;
|
||
}
|
||
|
||
if !line.is_empty() {
|
||
let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
|
||
let text_runs: &[TextRun] = if segments.is_empty() {
|
||
&styles
|
||
} else {
|
||
&Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
|
||
};
|
||
let shaped_line = window.text_system().shape_line(
|
||
line.clone().into(),
|
||
font_size,
|
||
text_runs,
|
||
None,
|
||
);
|
||
width += shaped_line.width;
|
||
len += shaped_line.len;
|
||
fragments.push(LineFragment::Text(shaped_line));
|
||
line.clear();
|
||
styles.clear();
|
||
}
|
||
|
||
match replacement {
|
||
ChunkReplacement::Renderer(renderer) => {
|
||
let available_width = if renderer.constrain_width {
|
||
let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
|
||
ellipsis.clone()
|
||
} else {
|
||
SharedString::from(Arc::from(highlighted_chunk.text))
|
||
};
|
||
let shaped_line = window.text_system().shape_line(
|
||
chunk,
|
||
font_size,
|
||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||
None,
|
||
);
|
||
AvailableSpace::Definite(shaped_line.width)
|
||
} else {
|
||
AvailableSpace::MinContent
|
||
};
|
||
|
||
let mut element = (renderer.render)(&mut ChunkRendererContext {
|
||
context: cx,
|
||
window,
|
||
max_width: text_width,
|
||
});
|
||
let line_height = text_style.line_height_in_pixels(window.rem_size());
|
||
let size = element.layout_as_root(
|
||
size(available_width, AvailableSpace::Definite(line_height)),
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
width += size.width;
|
||
len += highlighted_chunk.text.len();
|
||
line_byte_offset += highlighted_chunk.text.len();
|
||
fragments.push(LineFragment::Element {
|
||
id: renderer.id,
|
||
element: Some(element),
|
||
size,
|
||
len: highlighted_chunk.text.len(),
|
||
});
|
||
}
|
||
ChunkReplacement::Str(x) => {
|
||
let text_style = if let Some(style) = highlighted_chunk.style {
|
||
Cow::Owned(text_style.clone().highlight(style))
|
||
} else {
|
||
Cow::Borrowed(text_style)
|
||
};
|
||
|
||
let run = TextRun {
|
||
len: x.len(),
|
||
font: text_style.font(),
|
||
color: text_style.color,
|
||
background_color: text_style.background_color,
|
||
underline: text_style.underline,
|
||
strikethrough: text_style.strikethrough,
|
||
};
|
||
let line_layout = window
|
||
.text_system()
|
||
.shape_line(x, font_size, &[run], None)
|
||
.with_len(highlighted_chunk.text.len());
|
||
|
||
width += line_layout.width;
|
||
len += highlighted_chunk.text.len();
|
||
line_byte_offset += highlighted_chunk.text.len();
|
||
fragments.push(LineFragment::Text(line_layout))
|
||
}
|
||
}
|
||
} else {
|
||
for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
|
||
if ix > 0 {
|
||
let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]);
|
||
let text_runs = if segments.is_empty() {
|
||
&styles
|
||
} else {
|
||
&Self::split_runs_by_bg_segments(&styles, segments, min_contrast, len)
|
||
};
|
||
let shaped_line = window.text_system().shape_line(
|
||
line.clone().into(),
|
||
font_size,
|
||
text_runs,
|
||
None,
|
||
);
|
||
width += shaped_line.width;
|
||
len += shaped_line.len;
|
||
fragments.push(LineFragment::Text(shaped_line));
|
||
layouts.push(Self {
|
||
width: mem::take(&mut width),
|
||
len: mem::take(&mut len),
|
||
fragments: mem::take(&mut fragments),
|
||
invisibles: std::mem::take(&mut invisibles),
|
||
font_size,
|
||
});
|
||
|
||
line.clear();
|
||
line_byte_offset = 0;
|
||
styles.clear();
|
||
row += 1;
|
||
line_exceeded_max_len = false;
|
||
non_whitespace_added = false;
|
||
if row == max_line_count {
|
||
return layouts;
|
||
}
|
||
}
|
||
|
||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||
let text_style = if let Some(style) = highlighted_chunk.style {
|
||
Cow::Owned(text_style.clone().highlight(style))
|
||
} else {
|
||
Cow::Borrowed(text_style)
|
||
};
|
||
|
||
let current_line_len = len + line.len();
|
||
if current_line_len + line_chunk.len() > max_line_len {
|
||
let mut chunk_len = max_line_len - current_line_len;
|
||
while !line_chunk.is_char_boundary(chunk_len) {
|
||
chunk_len -= 1;
|
||
}
|
||
line_chunk = &line_chunk[..chunk_len];
|
||
line_exceeded_max_len = true;
|
||
}
|
||
|
||
if line_chunk.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
styles.push(TextRun {
|
||
len: line_chunk.len(),
|
||
font: text_style.font(),
|
||
color: text_style.color,
|
||
background_color: text_style.background_color,
|
||
underline: text_style.underline,
|
||
strikethrough: text_style.strikethrough,
|
||
});
|
||
|
||
if editor_mode.is_full() && !highlighted_chunk.is_inlay {
|
||
// Line wrap pads its contents with fake whitespaces,
|
||
// avoid printing them
|
||
let is_soft_wrapped = is_row_soft_wrapped(row);
|
||
if highlighted_chunk.is_tab {
|
||
if non_whitespace_added || !is_soft_wrapped {
|
||
invisibles.push(Invisible::Tab {
|
||
line_start_offset: line_byte_offset,
|
||
line_end_offset: line_byte_offset + line_chunk.len(),
|
||
});
|
||
}
|
||
} else {
|
||
invisibles.extend(line_chunk.char_indices().filter_map(
|
||
|(index, c)| {
|
||
let is_whitespace = c.is_whitespace();
|
||
non_whitespace_added |= !is_whitespace;
|
||
if is_whitespace
|
||
&& (non_whitespace_added || !is_soft_wrapped)
|
||
{
|
||
Some(Invisible::Whitespace {
|
||
line_start_offset: line_byte_offset + index,
|
||
line_end_offset: line_byte_offset
|
||
+ index
|
||
+ c.len_utf8(),
|
||
})
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
))
|
||
}
|
||
}
|
||
|
||
line.push_str(line_chunk);
|
||
line_byte_offset += line_chunk.len();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
layouts
|
||
}
|
||
|
||
/// Takes text runs and non-overlapping left-to-right background ranges with color.
|
||
/// Returns new text runs with adjusted contrast as per background ranges.
|
||
fn split_runs_by_bg_segments(
|
||
text_runs: &[TextRun],
|
||
bg_segments: &[(Range<DisplayPoint>, Hsla)],
|
||
min_contrast: f32,
|
||
start_col_offset: usize,
|
||
) -> Vec<TextRun> {
|
||
let mut output_runs: Vec<TextRun> = Vec::with_capacity(text_runs.len());
|
||
let mut line_col = start_col_offset;
|
||
let mut segment_ix = 0usize;
|
||
|
||
for text_run in text_runs.iter() {
|
||
let run_start_col = line_col;
|
||
let run_end_col = run_start_col + text_run.len;
|
||
while segment_ix < bg_segments.len()
|
||
&& (bg_segments[segment_ix].0.end.column() as usize) <= run_start_col
|
||
{
|
||
segment_ix += 1;
|
||
}
|
||
let mut cursor_col = run_start_col;
|
||
let mut local_segment_ix = segment_ix;
|
||
while local_segment_ix < bg_segments.len() {
|
||
let (range, segment_color) = &bg_segments[local_segment_ix];
|
||
let segment_start_col = range.start.column() as usize;
|
||
let segment_end_col = range.end.column() as usize;
|
||
if segment_start_col >= run_end_col {
|
||
break;
|
||
}
|
||
if segment_start_col > cursor_col {
|
||
let span_len = segment_start_col - cursor_col;
|
||
output_runs.push(TextRun {
|
||
len: span_len,
|
||
font: text_run.font.clone(),
|
||
color: text_run.color,
|
||
background_color: text_run.background_color,
|
||
underline: text_run.underline,
|
||
strikethrough: text_run.strikethrough,
|
||
});
|
||
cursor_col = segment_start_col;
|
||
}
|
||
let segment_slice_end_col = segment_end_col.min(run_end_col);
|
||
if segment_slice_end_col > cursor_col {
|
||
let new_text_color =
|
||
ensure_minimum_contrast(text_run.color, *segment_color, min_contrast);
|
||
output_runs.push(TextRun {
|
||
len: segment_slice_end_col - cursor_col,
|
||
font: text_run.font.clone(),
|
||
color: new_text_color,
|
||
background_color: text_run.background_color,
|
||
underline: text_run.underline,
|
||
strikethrough: text_run.strikethrough,
|
||
});
|
||
cursor_col = segment_slice_end_col;
|
||
}
|
||
if segment_end_col >= run_end_col {
|
||
break;
|
||
}
|
||
local_segment_ix += 1;
|
||
}
|
||
if cursor_col < run_end_col {
|
||
output_runs.push(TextRun {
|
||
len: run_end_col - cursor_col,
|
||
font: text_run.font.clone(),
|
||
color: text_run.color,
|
||
background_color: text_run.background_color,
|
||
underline: text_run.underline,
|
||
strikethrough: text_run.strikethrough,
|
||
});
|
||
}
|
||
line_col = run_end_col;
|
||
segment_ix = local_segment_ix;
|
||
}
|
||
output_runs
|
||
}
|
||
|
||
fn prepaint(
|
||
&mut self,
|
||
line_height: Pixels,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
row: DisplayRow,
|
||
content_origin: gpui::Point<Pixels>,
|
||
line_elements: &mut SmallVec<[AnyElement; 1]>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let line_y = f32::from(line_height) * Pixels::from(row.as_f64() - scroll_position.y);
|
||
self.prepaint_with_custom_offset(
|
||
line_height,
|
||
scroll_pixel_position,
|
||
content_origin,
|
||
line_y,
|
||
line_elements,
|
||
window,
|
||
cx,
|
||
);
|
||
}
|
||
|
||
fn prepaint_with_custom_offset(
|
||
&mut self,
|
||
line_height: Pixels,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
content_origin: gpui::Point<Pixels>,
|
||
line_y: Pixels,
|
||
line_elements: &mut SmallVec<[AnyElement; 1]>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let mut fragment_origin =
|
||
content_origin + gpui::point(Pixels::from(-scroll_pixel_position.x), line_y);
|
||
for fragment in &mut self.fragments {
|
||
match fragment {
|
||
LineFragment::Text(line) => {
|
||
fragment_origin.x += line.width;
|
||
}
|
||
LineFragment::Element { element, size, .. } => {
|
||
let mut element = element
|
||
.take()
|
||
.expect("you can't prepaint LineWithInvisibles twice");
|
||
|
||
// Center the element vertically within the line.
|
||
let mut element_origin = fragment_origin;
|
||
element_origin.y += (line_height - size.height) / 2.;
|
||
element.prepaint_at(element_origin, window, cx);
|
||
line_elements.push(element);
|
||
|
||
fragment_origin.x += size.width;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn draw(
|
||
&self,
|
||
layout: &EditorLayout,
|
||
row: DisplayRow,
|
||
content_origin: gpui::Point<Pixels>,
|
||
whitespace_setting: ShowWhitespaceSetting,
|
||
selection_ranges: &[Range<DisplayPoint>],
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
self.draw_with_custom_offset(
|
||
layout,
|
||
row,
|
||
content_origin,
|
||
layout.position_map.line_height
|
||
* (row.as_f64() - layout.position_map.scroll_position.y) as f32,
|
||
whitespace_setting,
|
||
selection_ranges,
|
||
window,
|
||
cx,
|
||
);
|
||
}
|
||
|
||
fn draw_with_custom_offset(
|
||
&self,
|
||
layout: &EditorLayout,
|
||
row: DisplayRow,
|
||
content_origin: gpui::Point<Pixels>,
|
||
line_y: Pixels,
|
||
whitespace_setting: ShowWhitespaceSetting,
|
||
selection_ranges: &[Range<DisplayPoint>],
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let line_height = layout.position_map.line_height;
|
||
let mut fragment_origin = content_origin
|
||
+ gpui::point(
|
||
Pixels::from(-layout.position_map.scroll_pixel_position.x),
|
||
line_y,
|
||
);
|
||
|
||
for fragment in &self.fragments {
|
||
match fragment {
|
||
LineFragment::Text(line) => {
|
||
line.paint(
|
||
fragment_origin,
|
||
line_height,
|
||
layout.text_align,
|
||
Some(layout.content_width),
|
||
window,
|
||
cx,
|
||
)
|
||
.log_err();
|
||
fragment_origin.x += line.width;
|
||
}
|
||
LineFragment::Element { size, .. } => {
|
||
fragment_origin.x += size.width;
|
||
}
|
||
}
|
||
}
|
||
|
||
self.draw_invisibles(
|
||
selection_ranges,
|
||
layout,
|
||
content_origin,
|
||
line_y,
|
||
row,
|
||
line_height,
|
||
whitespace_setting,
|
||
window,
|
||
cx,
|
||
);
|
||
}
|
||
|
||
fn draw_background(
|
||
&self,
|
||
layout: &EditorLayout,
|
||
row: DisplayRow,
|
||
content_origin: gpui::Point<Pixels>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let line_height = layout.position_map.line_height;
|
||
let line_y = line_height * (row.as_f64() - layout.position_map.scroll_position.y) as f32;
|
||
|
||
let mut fragment_origin = content_origin
|
||
+ gpui::point(
|
||
Pixels::from(-layout.position_map.scroll_pixel_position.x),
|
||
line_y,
|
||
);
|
||
|
||
for fragment in &self.fragments {
|
||
match fragment {
|
||
LineFragment::Text(line) => {
|
||
line.paint_background(
|
||
fragment_origin,
|
||
line_height,
|
||
layout.text_align,
|
||
Some(layout.content_width),
|
||
window,
|
||
cx,
|
||
)
|
||
.log_err();
|
||
fragment_origin.x += line.width;
|
||
}
|
||
LineFragment::Element { size, .. } => {
|
||
fragment_origin.x += size.width;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn draw_invisibles(
|
||
&self,
|
||
selection_ranges: &[Range<DisplayPoint>],
|
||
layout: &EditorLayout,
|
||
content_origin: gpui::Point<Pixels>,
|
||
line_y: Pixels,
|
||
row: DisplayRow,
|
||
line_height: Pixels,
|
||
whitespace_setting: ShowWhitespaceSetting,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
let extract_whitespace_info = |invisible: &Invisible| {
|
||
let (token_offset, token_end_offset, invisible_symbol) = match invisible {
|
||
Invisible::Tab {
|
||
line_start_offset,
|
||
line_end_offset,
|
||
} => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
|
||
Invisible::Whitespace {
|
||
line_start_offset,
|
||
line_end_offset,
|
||
} => (
|
||
*line_start_offset,
|
||
*line_end_offset,
|
||
&layout.space_invisible,
|
||
),
|
||
};
|
||
|
||
let token_x = self.x_for_index(token_offset);
|
||
// Center the marker inside the actual glyph's width so it lines up with
|
||
// proportional fonts instead of assuming a monospace `em_width` cell.
|
||
let glyph_width = (self.x_for_index(token_end_offset) - token_x).max(Pixels::ZERO);
|
||
let x_offset: ScrollPixelOffset = token_x.into();
|
||
let invisible_offset: ScrollPixelOffset =
|
||
((glyph_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0).into();
|
||
let origin = content_origin
|
||
+ gpui::point(
|
||
Pixels::from(
|
||
x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x,
|
||
),
|
||
line_y,
|
||
);
|
||
|
||
(
|
||
[token_offset, token_end_offset],
|
||
Box::new(move |window: &mut Window, cx: &mut App| {
|
||
invisible_symbol
|
||
.paint(origin, line_height, TextAlign::Left, None, window, cx)
|
||
.log_err();
|
||
}),
|
||
)
|
||
};
|
||
|
||
let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
|
||
match whitespace_setting {
|
||
ShowWhitespaceSetting::None => (),
|
||
ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(window, cx)),
|
||
ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
|
||
let invisible_point = DisplayPoint::new(row, start as u32);
|
||
if !selection_ranges
|
||
.iter()
|
||
.any(|region| region.start <= invisible_point && invisible_point < region.end)
|
||
{
|
||
return;
|
||
}
|
||
|
||
paint(window, cx);
|
||
}),
|
||
|
||
ShowWhitespaceSetting::Trailing => {
|
||
let mut previous_start = self.len;
|
||
for ([start, end], paint) in invisible_iter.rev() {
|
||
if previous_start != end {
|
||
break;
|
||
}
|
||
previous_start = start;
|
||
paint(window, cx);
|
||
}
|
||
}
|
||
|
||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||
// - It is a tab
|
||
// - It is adjacent to an edge (start or end)
|
||
// - It is adjacent to a whitespace (left or right)
|
||
ShowWhitespaceSetting::Boundary => {
|
||
// 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
|
||
// the above cases.
|
||
// Note: We zip in the original `invisibles` to check for tab equality
|
||
let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut Window, &mut App)>)> = None;
|
||
for (([start, end], paint), invisible) in
|
||
invisible_iter.zip_eq(self.invisibles.iter())
|
||
{
|
||
let should_render = match (&last_seen, invisible) {
|
||
(_, Invisible::Tab { .. }) => true,
|
||
(Some((_, last_end, _)), _) => *last_end == start,
|
||
_ => false,
|
||
};
|
||
|
||
if should_render || start == 0 || end == self.len {
|
||
paint(window, cx);
|
||
|
||
// Since we are scanning from the left, we will skip over the first available whitespace that is part
|
||
// of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
|
||
if let Some((should_render_last, last_end, paint_last)) = last_seen {
|
||
// Note that we need to make sure that the last one is actually adjacent
|
||
if !should_render_last && last_end == start {
|
||
paint_last(window, cx);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Manually render anything within a selection
|
||
let invisible_point = DisplayPoint::new(row, start as u32);
|
||
if selection_ranges.iter().any(|region| {
|
||
region.start <= invisible_point && invisible_point < region.end
|
||
}) {
|
||
paint(window, cx);
|
||
}
|
||
|
||
last_seen = Some((should_render, end, paint));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn x_for_index(&self, index: usize) -> Pixels {
|
||
let mut fragment_start_x = Pixels::ZERO;
|
||
let mut fragment_start_index = 0;
|
||
|
||
for fragment in &self.fragments {
|
||
match fragment {
|
||
LineFragment::Text(shaped_line) => {
|
||
let fragment_end_index = fragment_start_index + shaped_line.len;
|
||
if index < fragment_end_index {
|
||
return fragment_start_x
|
||
+ shaped_line.x_for_index(index - fragment_start_index);
|
||
}
|
||
fragment_start_x += shaped_line.width;
|
||
fragment_start_index = fragment_end_index;
|
||
}
|
||
LineFragment::Element { len, size, .. } => {
|
||
let fragment_end_index = fragment_start_index + len;
|
||
if index < fragment_end_index {
|
||
return fragment_start_x;
|
||
}
|
||
fragment_start_x += size.width;
|
||
fragment_start_index = fragment_end_index;
|
||
}
|
||
}
|
||
}
|
||
|
||
fragment_start_x
|
||
}
|
||
|
||
pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
|
||
let mut fragment_start_x = Pixels::ZERO;
|
||
let mut fragment_start_index = 0;
|
||
|
||
for fragment in &self.fragments {
|
||
match fragment {
|
||
LineFragment::Text(shaped_line) => {
|
||
let fragment_end_x = fragment_start_x + shaped_line.width;
|
||
if x < fragment_end_x {
|
||
return Some(
|
||
fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
|
||
);
|
||
}
|
||
fragment_start_x = fragment_end_x;
|
||
fragment_start_index += shaped_line.len;
|
||
}
|
||
LineFragment::Element { len, size, .. } => {
|
||
let fragment_end_x = fragment_start_x + size.width;
|
||
if x < fragment_end_x {
|
||
return Some(fragment_start_index);
|
||
}
|
||
fragment_start_index += len;
|
||
fragment_start_x = fragment_end_x;
|
||
}
|
||
}
|
||
}
|
||
|
||
None
|
||
}
|
||
|
||
pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
|
||
let mut fragment_start_index = 0;
|
||
|
||
for fragment in &self.fragments {
|
||
match fragment {
|
||
LineFragment::Text(shaped_line) => {
|
||
let fragment_end_index = fragment_start_index + shaped_line.len;
|
||
if index < fragment_end_index {
|
||
return shaped_line.font_id_for_index(index - fragment_start_index);
|
||
}
|
||
fragment_start_index = fragment_end_index;
|
||
}
|
||
LineFragment::Element { len, .. } => {
|
||
let fragment_end_index = fragment_start_index + len;
|
||
if index < fragment_end_index {
|
||
return None;
|
||
}
|
||
fragment_start_index = fragment_end_index;
|
||
}
|
||
}
|
||
}
|
||
|
||
None
|
||
}
|
||
|
||
pub fn alignment_offset(&self, text_align: TextAlign, content_width: Pixels) -> Pixels {
|
||
let line_width = self.width;
|
||
match text_align {
|
||
TextAlign::Left => px(0.0),
|
||
TextAlign::Center => (content_width - line_width) / 2.0,
|
||
TextAlign::Right => content_width - line_width,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
enum Invisible {
|
||
/// A tab character
|
||
///
|
||
/// A tab character is internally represented by spaces (configured by the user's tab width)
|
||
/// aligned to the nearest column, so it's necessary to store the start and end offset for
|
||
/// adjacency checks.
|
||
Tab {
|
||
line_start_offset: usize,
|
||
line_end_offset: usize,
|
||
},
|
||
/// A whitespace character (ASCII space or any other Unicode whitespace).
|
||
///
|
||
/// Storing both offsets correctly accounts for multi-byte whitespace characters
|
||
/// such as U+00A0 NO-BREAK SPACE, keeping adjacency checks correct.
|
||
Whitespace {
|
||
line_start_offset: usize,
|
||
line_end_offset: usize,
|
||
},
|
||
}
|
||
|
||
impl EditorElement {
|
||
/// Returns the rem size to use when rendering the [`EditorElement`].
|
||
///
|
||
/// This allows UI elements to scale based on the `buffer_font_size`.
|
||
fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
|
||
match self.editor.read(cx).mode {
|
||
EditorMode::Full {
|
||
scale_ui_elements_with_buffer_font_size: true,
|
||
..
|
||
}
|
||
| EditorMode::Minimap { .. } => {
|
||
let buffer_font_size = self.style.text.font_size;
|
||
match buffer_font_size {
|
||
AbsoluteLength::Pixels(pixels) => {
|
||
let rem_size_scale = {
|
||
// Our default UI font size is 14px on a 16px base scale.
|
||
// This means the default UI font size is 0.875rems.
|
||
let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
|
||
|
||
// We then determine the delta between a single rem and the default font
|
||
// size scale.
|
||
let default_font_size_delta = 1. - default_font_size_scale;
|
||
|
||
// Finally, we add this delta to 1rem to get the scale factor that
|
||
// should be used to scale up the UI.
|
||
1. + default_font_size_delta
|
||
};
|
||
|
||
Some(pixels * rem_size_scale)
|
||
}
|
||
AbsoluteLength::Rems(rems) => {
|
||
Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into()))
|
||
}
|
||
}
|
||
}
|
||
// We currently use single-line and auto-height editors in UI contexts,
|
||
// so we don't want to scale everything with the buffer font size, as it
|
||
// ends up looking off.
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn editor_with_selections(&self, cx: &App) -> Option<Entity<Editor>> {
|
||
if let EditorMode::Minimap { parent } = self.editor.read(cx).mode() {
|
||
parent.upgrade()
|
||
} else {
|
||
Some(self.editor.clone())
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Default)]
|
||
pub struct EditorRequestLayoutState {
|
||
// We use prepaint depth to limit the number of times prepaint is
|
||
// called recursively. We need this so that we can update stale
|
||
// data for e.g. block heights in block map.
|
||
prepaint_depth: Rc<Cell<usize>>,
|
||
}
|
||
|
||
impl EditorRequestLayoutState {
|
||
// In ideal conditions we only need one more subsequent prepaint call for resize to take effect.
|
||
// i.e. MAX_PREPAINT_DEPTH = 2, but placing near blocks can expose more lines from below, and
|
||
// we end up querying blocks for those lines too in subsequent renders.
|
||
// Setting MAX_PREPAINT_DEPTH = 3, passes all tests. Just to be on the safe side we set it to 5, so
|
||
// that subsequent shrinking does not lead to incorrect block placing.
|
||
const MAX_PREPAINT_DEPTH: usize = 5;
|
||
|
||
fn increment_prepaint_depth(&self) -> EditorPrepaintGuard {
|
||
let depth = self.prepaint_depth.get();
|
||
self.prepaint_depth.set(depth + 1);
|
||
EditorPrepaintGuard {
|
||
prepaint_depth: self.prepaint_depth.clone(),
|
||
}
|
||
}
|
||
|
||
fn has_remaining_prepaint_depth(&self) -> bool {
|
||
self.prepaint_depth.get() < Self::MAX_PREPAINT_DEPTH
|
||
}
|
||
}
|
||
|
||
struct EditorPrepaintGuard {
|
||
prepaint_depth: Rc<Cell<usize>>,
|
||
}
|
||
|
||
impl Drop for EditorPrepaintGuard {
|
||
fn drop(&mut self) {
|
||
let depth = self.prepaint_depth.get();
|
||
self.prepaint_depth.set(depth.saturating_sub(1));
|
||
}
|
||
}
|
||
|
||
impl Element for EditorElement {
|
||
type RequestLayoutState = EditorRequestLayoutState;
|
||
type PrepaintState = EditorLayout;
|
||
|
||
fn id(&self) -> Option<ElementId> {
|
||
None
|
||
}
|
||
|
||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||
None
|
||
}
|
||
|
||
fn request_layout(
|
||
&mut self,
|
||
_: Option<&GlobalElementId>,
|
||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||
let rem_size = self.rem_size(cx);
|
||
window.with_rem_size(rem_size, |window| {
|
||
self.editor.update(cx, |editor, cx| {
|
||
editor.set_style(self.style.clone(), window, cx);
|
||
|
||
let layout_id = match editor.mode {
|
||
EditorMode::SingleLine => {
|
||
let rem_size = window.rem_size();
|
||
let height = self.style.text.line_height_in_pixels(rem_size);
|
||
let mut style = Style::default();
|
||
style.size.height = height.into();
|
||
style.size.width = relative(1.).into();
|
||
window.request_layout(style, None, cx)
|
||
}
|
||
EditorMode::AutoHeight {
|
||
min_lines,
|
||
max_lines,
|
||
} => {
|
||
let editor_handle = cx.entity();
|
||
window.request_measured_layout(
|
||
Style::default(),
|
||
move |known_dimensions, available_space, window, cx| {
|
||
editor_handle
|
||
.update(cx, |editor, cx| {
|
||
compute_auto_height_layout(
|
||
editor,
|
||
min_lines,
|
||
max_lines,
|
||
known_dimensions,
|
||
available_space.width,
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.unwrap_or_default()
|
||
},
|
||
)
|
||
}
|
||
EditorMode::Minimap { .. } => {
|
||
let mut style = Style::default();
|
||
style.size.width = relative(1.).into();
|
||
style.size.height = relative(1.).into();
|
||
window.request_layout(style, None, cx)
|
||
}
|
||
EditorMode::Full {
|
||
sizing_behavior, ..
|
||
} => {
|
||
let mut style = Style::default();
|
||
style.size.width = relative(1.).into();
|
||
if sizing_behavior == SizingBehavior::SizeByContent {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
let line_height =
|
||
self.style.text.line_height_in_pixels(window.rem_size());
|
||
let scroll_height =
|
||
(snapshot.max_point().row().next_row().0 as f32) * line_height;
|
||
style.size.height = scroll_height.into();
|
||
} else {
|
||
style.size.height = relative(1.).into();
|
||
}
|
||
window.request_layout(style, None, cx)
|
||
}
|
||
};
|
||
|
||
(layout_id, EditorRequestLayoutState::default())
|
||
})
|
||
})
|
||
}
|
||
|
||
fn prepaint(
|
||
&mut self,
|
||
_: Option<&GlobalElementId>,
|
||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||
bounds: Bounds<Pixels>,
|
||
request_layout: &mut Self::RequestLayoutState,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> Self::PrepaintState {
|
||
let _prepaint_depth_guard = request_layout.increment_prepaint_depth();
|
||
let text_style = TextStyleRefinement {
|
||
font_size: Some(self.style.text.font_size),
|
||
line_height: Some(self.style.text.line_height),
|
||
..Default::default()
|
||
};
|
||
|
||
let is_minimap = self.editor.read(cx).mode.is_minimap();
|
||
let is_singleton = self.editor.read(cx).buffer_kind(cx) == ItemBufferKind::Singleton;
|
||
|
||
if !is_minimap {
|
||
let focus_handle = self.editor.focus_handle(cx);
|
||
window.set_view_id(self.editor.entity_id());
|
||
window.set_focus_handle(&focus_handle, cx);
|
||
}
|
||
|
||
let rem_size = self.rem_size(cx);
|
||
window.with_rem_size(rem_size, |window| {
|
||
window.with_text_style(Some(text_style), |window| {
|
||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||
let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
|
||
(editor.snapshot(window, cx), editor.read_only(cx))
|
||
});
|
||
let style = &self.style;
|
||
|
||
let rem_size = window.rem_size();
|
||
let font_id = window.text_system().resolve_font(&style.text.font());
|
||
let font_size = style.text.font_size.to_pixels(rem_size);
|
||
let line_height = style.text.line_height_in_pixels(rem_size);
|
||
let em_width = window.text_system().em_width(font_id, font_size).unwrap();
|
||
let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
|
||
let em_layout_width = window.text_system().em_layout_width(font_id, font_size);
|
||
let glyph_grid_cell = size(em_advance, line_height);
|
||
|
||
let gutter_dimensions =
|
||
snapshot.gutter_dimensions(font_id, font_size, style, window, cx);
|
||
let text_width = bounds.size.width - gutter_dimensions.width;
|
||
|
||
let settings = EditorSettings::get_global(cx);
|
||
let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never;
|
||
let vertical_scrollbar_width = (scrollbars_shown
|
||
&& settings.scrollbar.axes.vertical
|
||
&& self.editor.read(cx).show_scrollbars.vertical)
|
||
.then_some(style.scrollbar_width)
|
||
.unwrap_or_default();
|
||
let minimap_width = self
|
||
.get_minimap_width(
|
||
&settings.minimap,
|
||
scrollbars_shown,
|
||
text_width,
|
||
em_width,
|
||
font_size,
|
||
rem_size,
|
||
cx,
|
||
)
|
||
.unwrap_or_default();
|
||
|
||
let right_margin = minimap_width + vertical_scrollbar_width;
|
||
|
||
let extended_right = 2 * em_width + right_margin;
|
||
let editor_width = text_width - gutter_dimensions.margin - extended_right;
|
||
let editor_margins = EditorMargins {
|
||
gutter: gutter_dimensions,
|
||
right: right_margin,
|
||
extended_right,
|
||
};
|
||
|
||
snapshot = self.editor.update(cx, |editor, cx| {
|
||
editor.last_bounds = Some(bounds);
|
||
editor.gutter_dimensions = gutter_dimensions;
|
||
editor.set_visible_line_count(
|
||
(bounds.size.height / line_height) as f64,
|
||
window,
|
||
cx,
|
||
);
|
||
editor.set_visible_column_count(f64::from(editor_width / em_advance));
|
||
|
||
if matches!(
|
||
editor.mode,
|
||
EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
|
||
) {
|
||
snapshot
|
||
} else {
|
||
let wrap_width = calculate_wrap_width(
|
||
editor.soft_wrap_mode(cx),
|
||
editor_width,
|
||
em_layout_width,
|
||
);
|
||
|
||
if editor.set_wrap_width(wrap_width, cx) {
|
||
editor.snapshot(window, cx)
|
||
} else {
|
||
snapshot
|
||
}
|
||
}
|
||
});
|
||
|
||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||
let gutter_hitbox = window.insert_hitbox(
|
||
gutter_bounds(bounds, gutter_dimensions),
|
||
HitboxBehavior::Normal,
|
||
);
|
||
let text_hitbox = window.insert_hitbox(
|
||
Bounds {
|
||
origin: gutter_hitbox.top_right(),
|
||
size: size(text_width, bounds.size.height),
|
||
},
|
||
HitboxBehavior::Normal,
|
||
);
|
||
|
||
// Offset the content_bounds from the text_bounds by the gutter margin (which
|
||
// is roughly half a character wide) to make hit testing work more like how we want.
|
||
let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO);
|
||
let content_origin = text_hitbox.origin + content_offset;
|
||
|
||
let height_in_lines = f64::from(bounds.size.height / line_height);
|
||
let max_row = snapshot.max_point().row().as_f64();
|
||
|
||
// Calculate how much of the editor is clipped by parent containers (e.g., List).
|
||
// This allows us to only render lines that are actually visible, which is
|
||
// critical for performance when large content-sized editors are inside Lists.
|
||
let visible_bounds = window.content_mask().bounds;
|
||
let visible_top = bounds.top().max(visible_bounds.top());
|
||
let visible_bottom = bounds.bottom().min(visible_bounds.bottom());
|
||
let clipped_top = (visible_top - bounds.top()).max(px(0.));
|
||
let visible_height = (visible_bottom - visible_top).max(px(0.));
|
||
let clipped_top_in_lines = f64::from(clipped_top / line_height);
|
||
let visible_height_in_lines = f64::from(visible_height / line_height);
|
||
|
||
// The max scroll position for the top of the window
|
||
let scroll_beyond_last_line = self.editor.read(cx).scroll_beyond_last_line(cx);
|
||
let max_scroll_top = match scroll_beyond_last_line {
|
||
ScrollBeyondLastLine::OnePage => max_row,
|
||
ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.).max(0.),
|
||
ScrollBeyondLastLine::VerticalScrollMargin => {
|
||
let settings = EditorSettings::get_global(cx);
|
||
(max_row - height_in_lines + 1. + settings.vertical_scroll_margin)
|
||
.max(0.)
|
||
}
|
||
};
|
||
|
||
let (
|
||
autoscroll_request,
|
||
autoscroll_containing_element,
|
||
needs_horizontal_autoscroll,
|
||
) = self.editor.update(cx, |editor, cx| {
|
||
let autoscroll_request = editor.scroll_manager.take_autoscroll_request();
|
||
|
||
let autoscroll_containing_element =
|
||
autoscroll_request.is_some() || editor.has_pending_selection();
|
||
|
||
let (needs_horizontal_autoscroll, was_scrolled) = editor
|
||
.autoscroll_vertically(
|
||
bounds,
|
||
line_height,
|
||
max_scroll_top,
|
||
autoscroll_request,
|
||
window,
|
||
cx,
|
||
);
|
||
if was_scrolled.0 {
|
||
snapshot = editor.snapshot(window, cx);
|
||
}
|
||
(
|
||
autoscroll_request,
|
||
autoscroll_containing_element,
|
||
needs_horizontal_autoscroll,
|
||
)
|
||
});
|
||
|
||
let mut scroll_position = snapshot.scroll_position();
|
||
if !line_height.is_zero() {
|
||
scroll_position.y = window
|
||
.pixel_snap_f64(scroll_position.y * f64::from(line_height))
|
||
/ f64::from(line_height);
|
||
}
|
||
// The scroll position is a fractional point, the whole number of which represents
|
||
// the top of the window in terms of display rows.
|
||
// We add clipped_top_in_lines to skip rows that are clipped by parent containers,
|
||
// but we don't modify scroll_position itself since the parent handles positioning.
|
||
let max_row = snapshot.max_point().row();
|
||
let start_row = cmp::min(
|
||
DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32),
|
||
max_row,
|
||
);
|
||
let end_row = cmp::min(
|
||
(scroll_position.y + clipped_top_in_lines + visible_height_in_lines).ceil()
|
||
as u32,
|
||
max_row.next_row().0,
|
||
);
|
||
let end_row = DisplayRow(end_row);
|
||
|
||
let row_infos = snapshot // note we only get the visual range
|
||
.row_infos(start_row)
|
||
.take((start_row..end_row).len())
|
||
.collect::<Vec<RowInfo>>();
|
||
let is_row_soft_wrapped = |row: usize| {
|
||
row_infos
|
||
.get(row)
|
||
.is_none_or(|info| info.buffer_row.is_none())
|
||
};
|
||
|
||
let start_anchor = if start_row == Default::default() {
|
||
Anchor::Min
|
||
} else {
|
||
snapshot.buffer_snapshot().anchor_before(
|
||
DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left),
|
||
)
|
||
};
|
||
let end_anchor = if end_row > max_row {
|
||
Anchor::Max
|
||
} else {
|
||
snapshot.buffer_snapshot().anchor_before(
|
||
DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right),
|
||
)
|
||
};
|
||
|
||
let mut highlighted_rows = self
|
||
.editor
|
||
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
|
||
|
||
let mut highlighted_ranges = self
|
||
.editor_with_selections(cx)
|
||
.map(|editor| {
|
||
if editor == self.editor {
|
||
editor.read(cx).background_highlights_in_range(
|
||
start_anchor..end_anchor,
|
||
&snapshot.display_snapshot,
|
||
cx.theme(),
|
||
)
|
||
} else {
|
||
editor.update(cx, |editor, cx| {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
let start_anchor = if start_row == Default::default() {
|
||
Anchor::Min
|
||
} else {
|
||
snapshot.buffer_snapshot().anchor_before(
|
||
DisplayPoint::new(start_row, 0)
|
||
.to_offset(&snapshot, Bias::Left),
|
||
)
|
||
};
|
||
let end_anchor = if end_row > max_row {
|
||
Anchor::Max
|
||
} else {
|
||
snapshot.buffer_snapshot().anchor_before(
|
||
DisplayPoint::new(end_row, 0)
|
||
.to_offset(&snapshot, Bias::Right),
|
||
)
|
||
};
|
||
|
||
editor.background_highlights_in_range(
|
||
start_anchor..end_anchor,
|
||
&snapshot.display_snapshot,
|
||
cx.theme(),
|
||
)
|
||
})
|
||
}
|
||
})
|
||
.unwrap_or_default();
|
||
|
||
struct DiffHunkHighlightColors {
|
||
filled_background: Hsla,
|
||
hollow_background: Hsla,
|
||
hollow_border: Hsla,
|
||
}
|
||
|
||
let colors = cx.theme().colors();
|
||
let added_diff_hunk_colors = DiffHunkHighlightColors {
|
||
filled_background: colors.editor_diff_hunk_added_background,
|
||
hollow_background: colors.editor_diff_hunk_added_hollow_background,
|
||
hollow_border: colors.editor_diff_hunk_added_hollow_border,
|
||
};
|
||
let deleted_diff_hunk_colors = DiffHunkHighlightColors {
|
||
filled_background: colors.editor_diff_hunk_deleted_background,
|
||
hollow_background: colors.editor_diff_hunk_deleted_hollow_background,
|
||
hollow_border: colors.editor_diff_hunk_deleted_hollow_border,
|
||
};
|
||
let drag_highlight_color = colors.editor_active_line_background;
|
||
let drag_border_color = colors.border_focused;
|
||
|
||
for (ix, row_info) in row_infos.iter().enumerate() {
|
||
let Some(diff_status) = row_info.diff_status else {
|
||
continue;
|
||
};
|
||
|
||
let diff_hunk_colors = match diff_status.kind {
|
||
DiffHunkStatusKind::Added => &added_diff_hunk_colors,
|
||
DiffHunkStatusKind::Deleted => &deleted_diff_hunk_colors,
|
||
DiffHunkStatusKind::Modified => {
|
||
debug_panic!("modified diff status for row info");
|
||
continue;
|
||
}
|
||
};
|
||
|
||
let hollow_highlight = LineHighlight {
|
||
background: diff_hunk_colors.hollow_background.into(),
|
||
border: Some(diff_hunk_colors.hollow_border),
|
||
include_gutter: true,
|
||
type_id: None,
|
||
};
|
||
|
||
let filled_highlight = LineHighlight {
|
||
background: solid_background(diff_hunk_colors.filled_background),
|
||
border: None,
|
||
include_gutter: true,
|
||
type_id: None,
|
||
};
|
||
|
||
let background = if Self::diff_hunk_hollow(diff_status, cx) {
|
||
hollow_highlight
|
||
} else {
|
||
filled_highlight
|
||
};
|
||
|
||
let base_display_point =
|
||
DisplayPoint::new(start_row + DisplayRow(ix as u32), 0);
|
||
|
||
highlighted_rows
|
||
.entry(base_display_point.row())
|
||
.or_insert(background);
|
||
}
|
||
|
||
// Add diff review drag selection highlight to text area
|
||
if let Some(drag_state) = &self.editor.read(cx).diff_review_drag_state {
|
||
let range = drag_state.row_range(&snapshot.display_snapshot);
|
||
let start_row = range.start().0;
|
||
let end_row = range.end().0;
|
||
let drag_highlight = LineHighlight {
|
||
background: solid_background(drag_highlight_color),
|
||
border: Some(drag_border_color),
|
||
include_gutter: true,
|
||
type_id: None,
|
||
};
|
||
for row_num in start_row..=end_row {
|
||
highlighted_rows
|
||
.entry(DisplayRow(row_num))
|
||
.or_insert(drag_highlight);
|
||
}
|
||
}
|
||
|
||
let highlighted_gutter_ranges =
|
||
self.editor.read(cx).gutter_highlights_in_range(
|
||
start_anchor..end_anchor,
|
||
&snapshot.display_snapshot,
|
||
cx,
|
||
);
|
||
|
||
let document_colors = self
|
||
.editor
|
||
.read(cx)
|
||
.colors
|
||
.as_ref()
|
||
.map(|colors| colors.editor_display_highlights(&snapshot));
|
||
let redacted_ranges = self.editor.read(cx).redacted_ranges(
|
||
start_anchor..end_anchor,
|
||
&snapshot.display_snapshot,
|
||
cx,
|
||
);
|
||
|
||
let (local_selections, selected_buffer_ids, latest_selection_anchors): (
|
||
Vec<Selection<Point>>,
|
||
Vec<BufferId>,
|
||
HashMap<BufferId, Anchor>,
|
||
) = self
|
||
.editor_with_selections(cx)
|
||
.map(|editor| {
|
||
editor.update(cx, |editor, cx| {
|
||
let all_selections =
|
||
editor.selections.all::<Point>(&snapshot.display_snapshot);
|
||
let all_anchor_selections =
|
||
editor.selections.all_anchors(&snapshot.display_snapshot);
|
||
let selected_buffer_ids =
|
||
if editor.buffer_kind(cx) == ItemBufferKind::Singleton {
|
||
Vec::new()
|
||
} else {
|
||
let mut selected_buffer_ids =
|
||
Vec::with_capacity(all_selections.len());
|
||
|
||
for selection in all_selections {
|
||
for buffer_id in snapshot
|
||
.buffer_snapshot()
|
||
.buffer_ids_for_range(selection.range())
|
||
{
|
||
if selected_buffer_ids.last() != Some(&buffer_id) {
|
||
selected_buffer_ids.push(buffer_id);
|
||
}
|
||
}
|
||
}
|
||
|
||
selected_buffer_ids
|
||
};
|
||
|
||
let mut selections = editor.selections.disjoint_in_range(
|
||
start_anchor..end_anchor,
|
||
&snapshot.display_snapshot,
|
||
);
|
||
selections
|
||
.extend(editor.selections.pending(&snapshot.display_snapshot));
|
||
|
||
let mut anchors_by_buffer: HashMap<BufferId, (usize, Anchor)> =
|
||
HashMap::default();
|
||
for selection in all_anchor_selections.iter() {
|
||
let head = selection.head();
|
||
if let Some((text_anchor, _)) =
|
||
snapshot.buffer_snapshot().anchor_to_buffer_anchor(head)
|
||
{
|
||
anchors_by_buffer
|
||
.entry(text_anchor.buffer_id)
|
||
.and_modify(|(latest_id, latest_anchor)| {
|
||
if selection.id > *latest_id {
|
||
*latest_id = selection.id;
|
||
*latest_anchor = head;
|
||
}
|
||
})
|
||
.or_insert((selection.id, head));
|
||
}
|
||
}
|
||
let latest_selection_anchors = anchors_by_buffer
|
||
.into_iter()
|
||
.map(|(buffer_id, (_, anchor))| (buffer_id, anchor))
|
||
.collect();
|
||
|
||
(selections, selected_buffer_ids, latest_selection_anchors)
|
||
})
|
||
})
|
||
.unwrap_or_else(|| (Vec::new(), Vec::new(), HashMap::default()));
|
||
|
||
let (selections, mut active_rows, newest_selection_head) = self
|
||
.layout_selections(
|
||
start_anchor,
|
||
end_anchor,
|
||
&local_selections,
|
||
&snapshot,
|
||
start_row,
|
||
end_row,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
// relative rows are based on newest selection, even outside the visible area
|
||
let current_selection_head = self.editor.update(cx, |editor, cx| {
|
||
(editor.selections.count() != 0).then(|| {
|
||
let newest = editor
|
||
.selections
|
||
.newest::<Point>(&editor.display_snapshot(cx));
|
||
|
||
SelectionLayout::new(
|
||
newest,
|
||
editor.selections.line_mode(),
|
||
editor.cursor_offset_on_selection,
|
||
editor.cursor_shape,
|
||
&snapshot,
|
||
true,
|
||
true,
|
||
None,
|
||
)
|
||
.head
|
||
.row()
|
||
})
|
||
});
|
||
|
||
let run_indicator_rows = self.editor.update(cx, |editor, cx| {
|
||
editor.active_run_indicators(start_row..end_row, window, cx)
|
||
});
|
||
|
||
let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
|
||
editor.active_breakpoints(start_row..end_row, window, cx)
|
||
});
|
||
|
||
for (display_row, (_, bp, state)) in &breakpoint_rows {
|
||
if bp.is_enabled() && state.is_none_or(|s| s.verified) {
|
||
active_rows.entry(*display_row).or_default().breakpoint = true;
|
||
}
|
||
}
|
||
|
||
let gutter = Gutter {
|
||
line_height,
|
||
range: start_row..end_row,
|
||
scroll_position,
|
||
dimensions: &gutter_dimensions,
|
||
hitbox: &gutter_hitbox,
|
||
snapshot: &snapshot,
|
||
row_infos: &row_infos,
|
||
};
|
||
|
||
let line_numbers = self.layout_line_numbers(
|
||
&gutter,
|
||
&active_rows,
|
||
current_selection_head,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
let mut expand_toggles =
|
||
window.with_element_namespace("expand_toggles", |window| {
|
||
self.layout_expand_toggles(
|
||
&gutter_hitbox,
|
||
gutter_dimensions,
|
||
em_width,
|
||
line_height,
|
||
scroll_position,
|
||
start_row,
|
||
&row_infos,
|
||
window,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let mut crease_toggles =
|
||
window.with_element_namespace("crease_toggles", |window| {
|
||
self.layout_crease_toggles(
|
||
start_row..end_row,
|
||
&row_infos,
|
||
&active_rows,
|
||
&snapshot,
|
||
window,
|
||
cx,
|
||
)
|
||
});
|
||
let crease_trailers =
|
||
window.with_element_namespace("crease_trailers", |window| {
|
||
self.layout_crease_trailers(
|
||
row_infos.iter().cloned(),
|
||
&snapshot,
|
||
window,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let display_hunks = self.layout_gutter_diff_hunks(
|
||
line_height,
|
||
&gutter_hitbox,
|
||
start_row..end_row,
|
||
&snapshot,
|
||
scroll_position,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
Self::layout_word_diff_highlights(
|
||
&display_hunks,
|
||
&row_infos,
|
||
start_row,
|
||
&snapshot,
|
||
&mut highlighted_ranges,
|
||
cx,
|
||
);
|
||
|
||
let bg_segments_per_row = Self::bg_segments_per_row(
|
||
start_row..end_row,
|
||
&selections,
|
||
highlighted_ranges.iter().cloned().chain(
|
||
document_colors
|
||
.iter()
|
||
.flat_map(|(_, colors)| colors.iter().cloned()),
|
||
),
|
||
self.style.background,
|
||
);
|
||
|
||
let mut line_layouts = Self::layout_lines(
|
||
start_row..end_row,
|
||
&snapshot,
|
||
&self.style,
|
||
editor_width,
|
||
is_row_soft_wrapped,
|
||
&bg_segments_per_row,
|
||
window,
|
||
cx,
|
||
);
|
||
let new_renderer_widths = (!is_minimap).then(|| {
|
||
line_layouts
|
||
.iter()
|
||
.flat_map(|layout| &layout.fragments)
|
||
.filter_map(|fragment| {
|
||
if let LineFragment::Element { id, size, .. } = fragment {
|
||
Some((*id, size.width))
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
});
|
||
let renderer_widths_changed = request_layout.has_remaining_prepaint_depth()
|
||
&& new_renderer_widths.is_some_and(|new_renderer_widths| {
|
||
self.editor.update(cx, |editor, cx| {
|
||
editor.update_renderer_widths(new_renderer_widths, cx)
|
||
})
|
||
});
|
||
if renderer_widths_changed {
|
||
return self.prepaint(
|
||
None,
|
||
_inspector_id,
|
||
bounds,
|
||
request_layout,
|
||
window,
|
||
cx,
|
||
);
|
||
}
|
||
|
||
let longest_line_blame_width = self
|
||
.editor
|
||
.update(cx, |editor, cx| {
|
||
if !editor.show_git_blame_inline {
|
||
return None;
|
||
}
|
||
let blame = editor.blame.as_ref()?;
|
||
let (_, blame_entry) = blame
|
||
.update(cx, |blame, cx| {
|
||
let row_infos =
|
||
snapshot.row_infos(snapshot.longest_row()).next()?;
|
||
blame.blame_for_rows(&[row_infos], cx).next()
|
||
})
|
||
.flatten()?;
|
||
let mut element = render_inline_blame_entry(blame_entry, style, cx)?;
|
||
let inline_blame_padding =
|
||
ProjectSettings::get_global(cx).git.inline_blame.padding as f32
|
||
* em_advance;
|
||
Some(
|
||
element
|
||
.layout_as_root(AvailableSpace::min_size(), window, cx)
|
||
.width
|
||
+ inline_blame_padding,
|
||
)
|
||
})
|
||
.unwrap_or(Pixels::ZERO);
|
||
|
||
let longest_line_width = layout_line(
|
||
snapshot.longest_row(),
|
||
&snapshot,
|
||
style,
|
||
editor_width,
|
||
is_row_soft_wrapped,
|
||
window,
|
||
cx,
|
||
)
|
||
.width;
|
||
|
||
let scrollbar_layout_information = ScrollbarLayoutInformation::new(
|
||
text_hitbox.bounds,
|
||
glyph_grid_cell,
|
||
size(
|
||
longest_line_width,
|
||
Pixels::from(max_row.as_f64() * f64::from(line_height)),
|
||
),
|
||
longest_line_blame_width,
|
||
EditorSettings::get_global(cx),
|
||
scroll_beyond_last_line,
|
||
);
|
||
|
||
let mut scroll_width = scrollbar_layout_information.scroll_range.width;
|
||
|
||
let sticky_header_excerpt = if snapshot.buffer_snapshot().show_headers() {
|
||
snapshot.sticky_header_excerpt(scroll_position.y)
|
||
} else {
|
||
None
|
||
};
|
||
let sticky_header_excerpt_id = sticky_header_excerpt
|
||
.as_ref()
|
||
.map(|top| top.excerpt.buffer_id());
|
||
|
||
let buffer = snapshot.buffer_snapshot();
|
||
let start_buffer_row = MultiBufferRow(start_anchor.to_point(&buffer).row);
|
||
let end_buffer_row = MultiBufferRow(end_anchor.to_point(&buffer).row);
|
||
|
||
let preliminary_scroll_pixel_position = point(
|
||
scroll_position.x * f64::from(em_layout_width),
|
||
scroll_position.y * f64::from(line_height),
|
||
);
|
||
let indent_guides = self.layout_indent_guides(
|
||
content_origin,
|
||
text_hitbox.origin,
|
||
start_buffer_row..end_buffer_row,
|
||
preliminary_scroll_pixel_position,
|
||
line_height,
|
||
&snapshot,
|
||
window,
|
||
cx,
|
||
);
|
||
let indent_guides_for_spacers = indent_guides.clone();
|
||
|
||
let blocks = (!is_minimap)
|
||
.then(|| {
|
||
window.with_element_namespace("blocks", |window| {
|
||
self.render_blocks(
|
||
start_row..end_row,
|
||
&snapshot,
|
||
&hitbox,
|
||
&text_hitbox,
|
||
editor_width,
|
||
&mut scroll_width,
|
||
&editor_margins,
|
||
em_width,
|
||
gutter_dimensions.full_width(),
|
||
line_height,
|
||
&mut line_layouts,
|
||
&local_selections,
|
||
&selected_buffer_ids,
|
||
&latest_selection_anchors,
|
||
is_row_soft_wrapped,
|
||
sticky_header_excerpt_id,
|
||
&indent_guides_for_spacers,
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
})
|
||
.unwrap_or_default();
|
||
let RenderBlocksOutput {
|
||
non_spacer_blocks: mut blocks,
|
||
mut spacer_blocks,
|
||
row_block_types,
|
||
resized_blocks,
|
||
} = blocks;
|
||
if let Some(resized_blocks) = resized_blocks {
|
||
if request_layout.has_remaining_prepaint_depth() {
|
||
self.editor.update(cx, |editor, cx| {
|
||
editor.resize_blocks(
|
||
resized_blocks,
|
||
autoscroll_request.map(|(autoscroll, _)| autoscroll),
|
||
cx,
|
||
)
|
||
});
|
||
return self.prepaint(
|
||
None,
|
||
_inspector_id,
|
||
bounds,
|
||
request_layout,
|
||
window,
|
||
cx,
|
||
);
|
||
} else {
|
||
debug_panic!(
|
||
"dropping block resize because prepaint depth \
|
||
limit was reached"
|
||
);
|
||
}
|
||
}
|
||
|
||
let sticky_buffer_header = if self.should_show_buffer_headers() {
|
||
sticky_header_excerpt.map(|sticky_header_excerpt| {
|
||
window.with_element_namespace("blocks", |window| {
|
||
self.layout_sticky_buffer_header(
|
||
sticky_header_excerpt,
|
||
scroll_position,
|
||
line_height,
|
||
right_margin,
|
||
&snapshot,
|
||
&hitbox,
|
||
&selected_buffer_ids,
|
||
&blocks,
|
||
&latest_selection_anchors,
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
})
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let scroll_max: gpui::Point<ScrollPixelOffset> = point(
|
||
ScrollPixelOffset::from(
|
||
((scroll_width - editor_width) / em_layout_width).max(0.0),
|
||
),
|
||
max_scroll_top,
|
||
);
|
||
|
||
self.editor.update(cx, |editor, cx| {
|
||
if editor.scroll_manager.clamp_scroll_left(scroll_max.x, cx) {
|
||
scroll_position.x = scroll_max.x.min(scroll_position.x);
|
||
}
|
||
|
||
if needs_horizontal_autoscroll.0
|
||
&& let Some(new_scroll_position) = editor.autoscroll_horizontally(
|
||
start_row,
|
||
editor_width,
|
||
scroll_width,
|
||
em_advance,
|
||
&line_layouts,
|
||
autoscroll_request,
|
||
window,
|
||
cx,
|
||
)
|
||
{
|
||
scroll_position.x = new_scroll_position.x;
|
||
}
|
||
});
|
||
|
||
if !em_layout_width.is_zero() {
|
||
scroll_position.x = window
|
||
.pixel_snap_f64(scroll_position.x * f64::from(em_layout_width))
|
||
/ f64::from(em_layout_width);
|
||
}
|
||
|
||
let scroll_pixel_position = point(
|
||
scroll_position.x * f64::from(em_layout_width),
|
||
scroll_position.y * f64::from(line_height),
|
||
);
|
||
let sticky_headers = if !is_minimap
|
||
&& is_singleton
|
||
&& EditorSettings::get_global(cx).sticky_scroll.enabled
|
||
{
|
||
let relative = self.editor.read(cx).relative_line_numbers(cx);
|
||
self.layout_sticky_headers(
|
||
&snapshot,
|
||
editor_width,
|
||
is_row_soft_wrapped,
|
||
line_height,
|
||
scroll_pixel_position,
|
||
content_origin,
|
||
&gutter_dimensions,
|
||
&gutter_hitbox,
|
||
&text_hitbox,
|
||
relative,
|
||
current_selection_head,
|
||
window,
|
||
cx,
|
||
)
|
||
} else {
|
||
None
|
||
};
|
||
let indent_guides =
|
||
if scroll_pixel_position != preliminary_scroll_pixel_position {
|
||
self.layout_indent_guides(
|
||
content_origin,
|
||
text_hitbox.origin,
|
||
start_buffer_row..end_buffer_row,
|
||
scroll_pixel_position,
|
||
line_height,
|
||
&snapshot,
|
||
window,
|
||
cx,
|
||
)
|
||
} else {
|
||
indent_guides
|
||
};
|
||
|
||
let crease_trailers =
|
||
window.with_element_namespace("crease_trailers", |window| {
|
||
self.prepaint_crease_trailers(
|
||
crease_trailers,
|
||
&line_layouts,
|
||
line_height,
|
||
content_origin,
|
||
scroll_pixel_position,
|
||
scroll_position,
|
||
start_row,
|
||
em_width,
|
||
window,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let (edit_prediction_popover, edit_prediction_popover_origin) = self
|
||
.editor
|
||
.update(cx, |editor, cx| {
|
||
editor.render_edit_prediction_popover(
|
||
&text_hitbox.bounds,
|
||
content_origin,
|
||
right_margin,
|
||
&snapshot,
|
||
start_row..end_row,
|
||
scroll_position.y,
|
||
scroll_position.y + height_in_lines,
|
||
&line_layouts,
|
||
line_height,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
newest_selection_head,
|
||
editor_width,
|
||
style,
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.unzip();
|
||
|
||
let mut inline_diagnostics = self.layout_inline_diagnostics(
|
||
&line_layouts,
|
||
&crease_trailers,
|
||
&row_block_types,
|
||
content_origin,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
edit_prediction_popover_origin,
|
||
start_row,
|
||
end_row,
|
||
line_height,
|
||
em_width,
|
||
style,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
let mut inline_blame_layout = None;
|
||
let mut inline_code_actions = None;
|
||
if let Some(newest_selection_head) = newest_selection_head {
|
||
let display_row = newest_selection_head.row();
|
||
if (start_row..end_row).contains(&display_row)
|
||
&& !row_block_types.contains_key(&display_row)
|
||
{
|
||
inline_code_actions = self.layout_inline_code_actions(
|
||
newest_selection_head,
|
||
content_origin,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
line_height,
|
||
&snapshot,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
let line_ix = display_row.minus(start_row) as usize;
|
||
if let (Some(row_info), Some(line_layout), Some(crease_trailer)) = (
|
||
row_infos.get(line_ix),
|
||
line_layouts.get(line_ix),
|
||
crease_trailers.get(line_ix),
|
||
) {
|
||
let crease_trailer_layout = crease_trailer.as_ref();
|
||
if let Some(layout) = self.layout_inline_blame(
|
||
display_row,
|
||
row_info,
|
||
line_layout,
|
||
crease_trailer_layout,
|
||
em_width,
|
||
content_origin,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
line_height,
|
||
window,
|
||
cx,
|
||
) {
|
||
inline_blame_layout = Some(layout);
|
||
// Blame overrides inline diagnostics
|
||
inline_diagnostics.remove(&display_row);
|
||
}
|
||
} else {
|
||
log::error!(
|
||
"bug: line_ix {} is out of bounds - row_infos.len(): {}, \
|
||
line_layouts.len(): {}, \
|
||
crease_trailers.len(): {}",
|
||
line_ix,
|
||
row_infos.len(),
|
||
line_layouts.len(),
|
||
crease_trailers.len(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
let blamed_display_rows = self.layout_blame_entries(
|
||
&row_infos,
|
||
em_width,
|
||
scroll_position,
|
||
start_row,
|
||
line_height,
|
||
&gutter_hitbox,
|
||
gutter_dimensions.git_blame_entries_width,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
let line_elements = self.prepaint_lines(
|
||
start_row,
|
||
&mut line_layouts,
|
||
line_height,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
content_origin,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
window.with_element_namespace("blocks", |window| {
|
||
self.layout_blocks(
|
||
&mut blocks,
|
||
&hitbox,
|
||
&gutter_hitbox,
|
||
line_height,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
&editor_margins,
|
||
window,
|
||
cx,
|
||
);
|
||
self.layout_blocks(
|
||
&mut spacer_blocks,
|
||
&hitbox,
|
||
&gutter_hitbox,
|
||
line_height,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
&editor_margins,
|
||
window,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let cursors = self.collect_cursors(&snapshot, cx);
|
||
let visible_row_range = start_row..end_row;
|
||
let non_visible_cursors = cursors
|
||
.iter()
|
||
.any(|c| !visible_row_range.contains(&c.0.row()));
|
||
|
||
let visible_cursors = self.layout_visible_cursors(
|
||
&snapshot,
|
||
&selections,
|
||
&row_block_types,
|
||
start_row..end_row,
|
||
&line_layouts,
|
||
&text_hitbox,
|
||
content_origin,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
line_height,
|
||
em_width,
|
||
em_advance,
|
||
autoscroll_containing_element,
|
||
&redacted_ranges,
|
||
window,
|
||
cx,
|
||
);
|
||
let navigation_overlay_paint_commands = self.layout_navigation_overlays(
|
||
&snapshot,
|
||
start_row..end_row,
|
||
&line_layouts,
|
||
&text_hitbox,
|
||
content_origin,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
line_height,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
let scrollbars_layout = self.layout_scrollbars(
|
||
&snapshot,
|
||
&scrollbar_layout_information,
|
||
content_offset,
|
||
scroll_position,
|
||
non_visible_cursors,
|
||
right_margin,
|
||
editor_width,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||
|
||
let context_menu_layout =
|
||
if let Some(newest_selection_head) = newest_selection_head {
|
||
let newest_selection_point =
|
||
newest_selection_head.to_point(&snapshot.display_snapshot);
|
||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||
self.layout_cursor_popovers(
|
||
line_height,
|
||
&text_hitbox,
|
||
content_origin,
|
||
right_margin,
|
||
start_row,
|
||
scroll_pixel_position,
|
||
&line_layouts,
|
||
newest_selection_head,
|
||
newest_selection_point,
|
||
style,
|
||
window,
|
||
cx,
|
||
)
|
||
} else {
|
||
None
|
||
}
|
||
} else {
|
||
None
|
||
};
|
||
|
||
self.layout_gutter_menu(
|
||
line_height,
|
||
&text_hitbox,
|
||
content_origin,
|
||
right_margin,
|
||
scroll_pixel_position,
|
||
gutter_dimensions.width - gutter_dimensions.left_padding,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
let test_indicators = if gutter_settings.runnables {
|
||
self.layout_run_indicators(
|
||
&gutter,
|
||
&run_indicator_rows,
|
||
&breakpoint_rows,
|
||
window,
|
||
cx,
|
||
)
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
|
||
let show_bookmarks =
|
||
snapshot.show_bookmarks.unwrap_or(gutter_settings.bookmarks);
|
||
|
||
let bookmark_rows = self.editor.update(cx, |editor, cx| {
|
||
let mut rows = editor.active_bookmarks(start_row..end_row, window, cx);
|
||
rows.retain(|k| !run_indicator_rows.contains(k));
|
||
rows.retain(|k| !breakpoint_rows.contains_key(k));
|
||
rows
|
||
});
|
||
|
||
let bookmarks = if show_bookmarks {
|
||
self.layout_bookmarks(&gutter, &bookmark_rows, window, cx)
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
|
||
let show_breakpoints = snapshot
|
||
.show_breakpoints
|
||
.unwrap_or(gutter_settings.breakpoints);
|
||
|
||
breakpoint_rows.retain(|k, _| !run_indicator_rows.contains(k));
|
||
let mut breakpoints = if show_breakpoints {
|
||
self.layout_breakpoints(&gutter, &breakpoint_rows, window, cx)
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
|
||
let gutter_hover_button = self
|
||
.editor
|
||
.read(cx)
|
||
.gutter_hover_button
|
||
.0
|
||
.filter(|phantom| phantom.is_active)
|
||
.map(|phantom| phantom.display_row);
|
||
|
||
if let Some(row) = gutter_hover_button
|
||
&& !breakpoint_rows.contains_key(&row)
|
||
&& !run_indicator_rows.contains(&row)
|
||
&& !bookmark_rows.contains(&row)
|
||
&& (show_bookmarks || show_breakpoints)
|
||
{
|
||
let position = snapshot
|
||
.display_point_to_anchor(DisplayPoint::new(row, 0), Bias::Right);
|
||
breakpoints.extend(
|
||
self.layout_gutter_hover_button(&gutter, position, row, window, cx),
|
||
);
|
||
}
|
||
|
||
let git_gutter_width = Self::gutter_strip_width(line_height)
|
||
+ gutter_dimensions
|
||
.git_blame_entries_width
|
||
.unwrap_or_default();
|
||
let available_width = gutter_dimensions.left_padding - git_gutter_width;
|
||
|
||
let max_line_number_length = self
|
||
.editor
|
||
.read(cx)
|
||
.buffer()
|
||
.read(cx)
|
||
.snapshot(cx)
|
||
.widest_line_number()
|
||
.ilog10()
|
||
+ 1;
|
||
|
||
let diff_review_button = self
|
||
.should_render_diff_review_button(
|
||
start_row..end_row,
|
||
&row_infos,
|
||
&snapshot,
|
||
cx,
|
||
)
|
||
.map(|(display_row, buffer_row)| {
|
||
let is_wide = max_line_number_length
|
||
>= EditorSettings::get_global(cx).gutter.min_line_number_digits
|
||
as u32
|
||
&& buffer_row.is_some_and(|row| {
|
||
(row + 1).ilog10() + 1 == max_line_number_length
|
||
})
|
||
|| gutter_dimensions.right_padding == px(0.);
|
||
|
||
let button_width = if is_wide {
|
||
available_width - px(6.)
|
||
} else {
|
||
available_width + em_width - px(6.)
|
||
};
|
||
|
||
let button = self.editor.update(cx, |editor, cx| {
|
||
editor
|
||
.render_diff_review_button(display_row, button_width, cx)
|
||
.into_any_element()
|
||
});
|
||
gutter.prepaint_button(button, display_row, window, cx)
|
||
});
|
||
|
||
self.layout_signature_help(
|
||
&hitbox,
|
||
content_origin,
|
||
scroll_pixel_position,
|
||
newest_selection_head,
|
||
start_row,
|
||
&line_layouts,
|
||
line_height,
|
||
em_width,
|
||
context_menu_layout,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
if !cx.has_active_drag() {
|
||
self.layout_hover_popovers(
|
||
&snapshot,
|
||
&hitbox,
|
||
start_row..end_row,
|
||
content_origin,
|
||
scroll_pixel_position,
|
||
&line_layouts,
|
||
line_height,
|
||
em_width,
|
||
context_menu_layout,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
self.layout_blame_popover(&snapshot, &hitbox, line_height, window, cx);
|
||
}
|
||
|
||
let mouse_context_menu = self.layout_mouse_context_menu(
|
||
&snapshot,
|
||
start_row..end_row,
|
||
content_origin,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
window.with_element_namespace("crease_toggles", |window| {
|
||
self.prepaint_crease_toggles(
|
||
&mut crease_toggles,
|
||
line_height,
|
||
&gutter_dimensions,
|
||
gutter_settings,
|
||
scroll_position,
|
||
start_row,
|
||
&gutter_hitbox,
|
||
window,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
window.with_element_namespace("expand_toggles", |window| {
|
||
self.prepaint_expand_toggles(&mut expand_toggles, window, cx)
|
||
});
|
||
|
||
let wrap_guides = self.layout_wrap_guides(
|
||
em_advance,
|
||
scroll_position,
|
||
content_origin,
|
||
scrollbars_layout.as_ref(),
|
||
vertical_scrollbar_width,
|
||
&hitbox,
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
let minimap = window.with_element_namespace("minimap", |window| {
|
||
self.layout_minimap(
|
||
&snapshot,
|
||
minimap_width,
|
||
scroll_position,
|
||
&scrollbar_layout_information,
|
||
scrollbars_layout.as_ref(),
|
||
window,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let invisible_symbol_font_size = font_size / 2.;
|
||
let whitespace_map = &self
|
||
.editor
|
||
.read(cx)
|
||
.buffer
|
||
.read(cx)
|
||
.language_settings(cx)
|
||
.whitespace_map;
|
||
|
||
let tab_char = whitespace_map.tab.clone();
|
||
let tab_len = tab_char.len();
|
||
let tab_invisible = window.text_system().shape_line(
|
||
tab_char,
|
||
invisible_symbol_font_size,
|
||
&[TextRun {
|
||
len: tab_len,
|
||
font: self.style.text.font(),
|
||
color: cx.theme().colors().editor_invisible,
|
||
..Default::default()
|
||
}],
|
||
None,
|
||
);
|
||
|
||
let space_char = whitespace_map.space.clone();
|
||
let space_len = space_char.len();
|
||
let space_invisible = window.text_system().shape_line(
|
||
space_char,
|
||
invisible_symbol_font_size,
|
||
&[TextRun {
|
||
len: space_len,
|
||
font: self.style.text.font(),
|
||
color: cx.theme().colors().editor_invisible,
|
||
..Default::default()
|
||
}],
|
||
None,
|
||
);
|
||
|
||
let mode = snapshot.mode.clone();
|
||
|
||
let sticky_scroll_header_height = sticky_headers
|
||
.as_ref()
|
||
.and_then(|headers| headers.lines.last())
|
||
.map_or(Pixels::ZERO, |last| last.offset + line_height);
|
||
|
||
let has_sticky_buffer_header =
|
||
sticky_buffer_header.is_some() || sticky_header_excerpt_id.is_some();
|
||
let sticky_header_height = if has_sticky_buffer_header {
|
||
let full_height = FILE_HEADER_HEIGHT as f32 * line_height;
|
||
let display_row = blocks
|
||
.iter()
|
||
.filter(|block| block.is_buffer_header)
|
||
.find_map(|block| {
|
||
block.row.filter(|row| row.0 > scroll_position.y as u32)
|
||
});
|
||
let offset = match display_row {
|
||
Some(display_row) => {
|
||
let max_row = display_row.0.saturating_sub(FILE_HEADER_HEIGHT);
|
||
let offset = (scroll_position.y - max_row as f64).max(0.0);
|
||
let slide_up =
|
||
Pixels::from(offset * ScrollPixelOffset::from(line_height));
|
||
|
||
(full_height - slide_up).max(Pixels::ZERO)
|
||
}
|
||
None => full_height,
|
||
};
|
||
let header_bottom_padding =
|
||
BUFFER_HEADER_PADDING.to_pixels(window.rem_size());
|
||
sticky_scroll_header_height + offset - header_bottom_padding
|
||
} else {
|
||
sticky_scroll_header_height
|
||
};
|
||
|
||
let (diff_hunk_controls, diff_hunk_control_bounds) =
|
||
if is_read_only && !self.editor.read(cx).delegate_stage_and_restore {
|
||
(vec![], vec![])
|
||
} else {
|
||
self.layout_diff_hunk_controls(
|
||
start_row..end_row,
|
||
&row_infos,
|
||
&text_hitbox,
|
||
current_selection_head,
|
||
line_height,
|
||
right_margin,
|
||
scroll_pixel_position,
|
||
sticky_header_height,
|
||
&display_hunks,
|
||
&highlighted_rows,
|
||
self.editor.clone(),
|
||
window,
|
||
cx,
|
||
)
|
||
};
|
||
|
||
let position_map = Rc::new(PositionMap {
|
||
size: bounds.size,
|
||
visible_row_range,
|
||
scroll_position,
|
||
scroll_pixel_position,
|
||
scroll_max,
|
||
line_layouts,
|
||
line_height,
|
||
em_advance,
|
||
em_layout_width,
|
||
snapshot,
|
||
text_align: self.style.text.text_align,
|
||
content_width: text_hitbox.size.width,
|
||
gutter_hitbox: gutter_hitbox.clone(),
|
||
text_hitbox: text_hitbox.clone(),
|
||
inline_blame_bounds: inline_blame_layout
|
||
.as_ref()
|
||
.map(|layout| (layout.bounds, layout.buffer_id, layout.entry.clone())),
|
||
display_hunks: display_hunks.clone(),
|
||
diff_hunk_control_bounds,
|
||
});
|
||
|
||
self.editor.update(cx, |editor, _| {
|
||
editor.last_position_map = Some(position_map.clone())
|
||
});
|
||
|
||
EditorLayout {
|
||
mode,
|
||
position_map,
|
||
visible_display_row_range: start_row..end_row,
|
||
wrap_guides,
|
||
indent_guides,
|
||
hitbox,
|
||
gutter_hitbox,
|
||
display_hunks,
|
||
content_origin,
|
||
scrollbars_layout,
|
||
minimap,
|
||
active_rows,
|
||
highlighted_rows,
|
||
highlighted_ranges,
|
||
highlighted_gutter_ranges,
|
||
redacted_ranges,
|
||
document_colors,
|
||
line_elements,
|
||
line_numbers,
|
||
blamed_display_rows,
|
||
inline_diagnostics,
|
||
inline_blame_layout,
|
||
inline_code_actions,
|
||
blocks,
|
||
spacer_blocks,
|
||
cursors,
|
||
visible_cursors,
|
||
navigation_overlay_paint_commands,
|
||
selections,
|
||
edit_prediction_popover,
|
||
diff_hunk_controls,
|
||
mouse_context_menu,
|
||
test_indicators,
|
||
bookmarks,
|
||
breakpoints,
|
||
diff_review_button,
|
||
crease_toggles,
|
||
crease_trailers,
|
||
tab_invisible,
|
||
space_invisible,
|
||
sticky_buffer_header,
|
||
sticky_headers,
|
||
expand_toggles,
|
||
text_align: self.style.text.text_align,
|
||
content_width: text_hitbox.size.width,
|
||
}
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
fn paint(
|
||
&mut self,
|
||
_: Option<&GlobalElementId>,
|
||
_inspector_id: Option<&gpui::InspectorElementId>,
|
||
bounds: Bounds<gpui::Pixels>,
|
||
_: &mut Self::RequestLayoutState,
|
||
layout: &mut Self::PrepaintState,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
if !layout.mode.is_minimap() {
|
||
let focus_handle = self.editor.focus_handle(cx);
|
||
let key_context = self
|
||
.editor
|
||
.update(cx, |editor, cx| editor.key_context(window, cx));
|
||
|
||
window.set_key_context(key_context);
|
||
window.handle_input(
|
||
&focus_handle,
|
||
ElementInputHandler::new(bounds, self.editor.clone()),
|
||
cx,
|
||
);
|
||
self.register_actions(window, cx);
|
||
self.register_key_listeners(window, cx, layout);
|
||
}
|
||
|
||
let text_style = TextStyleRefinement {
|
||
font_size: Some(self.style.text.font_size),
|
||
line_height: Some(self.style.text.line_height),
|
||
..Default::default()
|
||
};
|
||
let rem_size = self.rem_size(cx);
|
||
window.with_rem_size(rem_size, |window| {
|
||
window.with_text_style(Some(text_style), |window| {
|
||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||
self.paint_mouse_listeners(layout, window, cx);
|
||
|
||
// Mask the editor behind sticky scroll headers. Important
|
||
// for transparent backgrounds.
|
||
let below_sticky_headers_mask = layout
|
||
.sticky_headers
|
||
.as_ref()
|
||
.and_then(|h| h.lines.last())
|
||
.map(|last| ContentMask {
|
||
bounds: Bounds {
|
||
origin: point(
|
||
bounds.origin.x,
|
||
bounds.origin.y + last.offset + layout.position_map.line_height,
|
||
),
|
||
size: size(
|
||
bounds.size.width,
|
||
(bounds.size.height
|
||
- last.offset
|
||
- layout.position_map.line_height)
|
||
.max(Pixels::ZERO),
|
||
),
|
||
},
|
||
});
|
||
|
||
window.with_content_mask(below_sticky_headers_mask, |window| {
|
||
self.paint_background(layout, window, cx);
|
||
|
||
self.paint_indent_guides(layout, window, cx);
|
||
|
||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||
self.paint_blamed_display_rows(layout, window, cx);
|
||
self.paint_line_numbers(layout, window, cx);
|
||
}
|
||
|
||
self.paint_text(layout, window, cx);
|
||
|
||
if !layout.spacer_blocks.is_empty() {
|
||
window.with_element_namespace("blocks", |window| {
|
||
self.paint_spacer_blocks(layout, window, cx);
|
||
});
|
||
}
|
||
|
||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||
self.paint_gutter_highlights(layout, window, cx);
|
||
self.paint_gutter_indicators(layout, window, cx);
|
||
}
|
||
|
||
if !layout.blocks.is_empty() {
|
||
window.with_element_namespace("blocks", |window| {
|
||
self.paint_non_spacer_blocks(layout, window, cx);
|
||
});
|
||
}
|
||
});
|
||
|
||
window.with_element_namespace("blocks", |window| {
|
||
if let Some(mut sticky_header) = layout.sticky_buffer_header.take() {
|
||
sticky_header.paint(window, cx)
|
||
}
|
||
});
|
||
|
||
self.paint_sticky_headers(layout, window, cx);
|
||
self.paint_minimap(layout, window, cx);
|
||
self.paint_scrollbars(layout, window, cx);
|
||
self.paint_edit_prediction_popover(layout, window, cx);
|
||
self.paint_mouse_context_menu(layout, window, cx);
|
||
});
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
pub(super) fn gutter_bounds(
|
||
editor_bounds: Bounds<Pixels>,
|
||
gutter_dimensions: GutterDimensions,
|
||
) -> Bounds<Pixels> {
|
||
Bounds {
|
||
origin: editor_bounds.origin,
|
||
size: size(gutter_dimensions.width, editor_bounds.size.height),
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy)]
|
||
struct ContextMenuLayout {
|
||
y_flipped: bool,
|
||
bounds: Bounds<Pixels>,
|
||
}
|
||
|
||
/// Holds information required for layouting the editor scrollbars.
|
||
struct ScrollbarLayoutInformation {
|
||
/// The bounds of the editor area (excluding the content offset).
|
||
editor_bounds: Bounds<Pixels>,
|
||
/// The available range to scroll within the document.
|
||
scroll_range: Size<Pixels>,
|
||
/// The space available for one glyph in the editor.
|
||
glyph_grid_cell: Size<Pixels>,
|
||
}
|
||
|
||
impl ScrollbarLayoutInformation {
|
||
pub fn new(
|
||
editor_bounds: Bounds<Pixels>,
|
||
glyph_grid_cell: Size<Pixels>,
|
||
document_size: Size<Pixels>,
|
||
longest_line_blame_width: Pixels,
|
||
settings: &EditorSettings,
|
||
scroll_beyond_last_line: ScrollBeyondLastLine,
|
||
) -> Self {
|
||
let vertical_overscroll = match scroll_beyond_last_line {
|
||
ScrollBeyondLastLine::OnePage => editor_bounds.size.height,
|
||
ScrollBeyondLastLine::Off => glyph_grid_cell.height,
|
||
ScrollBeyondLastLine::VerticalScrollMargin => {
|
||
(1.0 + settings.vertical_scroll_margin) as f32 * glyph_grid_cell.height
|
||
}
|
||
};
|
||
|
||
let overscroll = size(longest_line_blame_width, vertical_overscroll);
|
||
|
||
ScrollbarLayoutInformation {
|
||
editor_bounds,
|
||
scroll_range: document_size + overscroll,
|
||
glyph_grid_cell,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl IntoElement for EditorElement {
|
||
type Element = Self;
|
||
|
||
fn into_element(self) -> Self::Element {
|
||
self
|
||
}
|
||
}
|
||
|
||
pub struct EditorLayout {
|
||
position_map: Rc<PositionMap>,
|
||
hitbox: Hitbox,
|
||
gutter_hitbox: Hitbox,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scrollbars_layout: Option<EditorScrollbars>,
|
||
minimap: Option<MinimapLayout>,
|
||
mode: EditorMode,
|
||
wrap_guides: SmallVec<[(Pixels, bool); 2]>,
|
||
indent_guides: Option<Vec<IndentGuideLayout>>,
|
||
visible_display_row_range: Range<DisplayRow>,
|
||
active_rows: BTreeMap<DisplayRow, LineHighlightSpec>,
|
||
highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
|
||
line_elements: SmallVec<[AnyElement; 1]>,
|
||
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
|
||
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||
blamed_display_rows: Option<Vec<AnyElement>>,
|
||
inline_diagnostics: HashMap<DisplayRow, AnyElement>,
|
||
inline_blame_layout: Option<InlineBlameLayout>,
|
||
inline_code_actions: Option<AnyElement>,
|
||
blocks: Vec<BlockLayout>,
|
||
spacer_blocks: Vec<BlockLayout>,
|
||
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||
redacted_ranges: Vec<Range<DisplayPoint>>,
|
||
cursors: Vec<(DisplayPoint, Hsla)>,
|
||
visible_cursors: Vec<CursorLayout>,
|
||
navigation_overlay_paint_commands: Vec<NavigationOverlayPaintCommand>,
|
||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||
test_indicators: Vec<AnyElement>,
|
||
bookmarks: Vec<AnyElement>,
|
||
breakpoints: Vec<AnyElement>,
|
||
diff_review_button: Option<AnyElement>,
|
||
crease_toggles: Vec<Option<AnyElement>>,
|
||
expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,
|
||
diff_hunk_controls: Vec<AnyElement>,
|
||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||
edit_prediction_popover: Option<AnyElement>,
|
||
mouse_context_menu: Option<AnyElement>,
|
||
tab_invisible: ShapedLine,
|
||
space_invisible: ShapedLine,
|
||
sticky_buffer_header: Option<AnyElement>,
|
||
sticky_headers: Option<header::StickyHeaders>,
|
||
document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
|
||
text_align: TextAlign,
|
||
content_width: Pixels,
|
||
}
|
||
|
||
impl EditorLayout {
|
||
fn line_end_overshoot(&self) -> Pixels {
|
||
0.15 * self.position_map.line_height
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
struct LineNumberSegment {
|
||
shaped_line: ShapedLine,
|
||
hitbox: Option<Hitbox>,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
struct LineNumberLayout {
|
||
segments: SmallVec<[LineNumberSegment; 1]>,
|
||
}
|
||
|
||
struct ColoredRange<T> {
|
||
start: T,
|
||
end: T,
|
||
color: Hsla,
|
||
}
|
||
|
||
impl Along for ScrollbarAxes {
|
||
type Unit = bool;
|
||
|
||
fn along(&self, axis: ScrollbarAxis) -> Self::Unit {
|
||
match axis {
|
||
ScrollbarAxis::Horizontal => self.horizontal,
|
||
ScrollbarAxis::Vertical => self.vertical,
|
||
}
|
||
}
|
||
|
||
fn apply_along(&self, axis: ScrollbarAxis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self {
|
||
match axis {
|
||
ScrollbarAxis::Horizontal => ScrollbarAxes {
|
||
horizontal: f(self.horizontal),
|
||
vertical: self.vertical,
|
||
},
|
||
ScrollbarAxis::Vertical => ScrollbarAxes {
|
||
horizontal: self.horizontal,
|
||
vertical: f(self.vertical),
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
struct EditorScrollbars {
|
||
pub vertical: Option<ScrollbarLayout>,
|
||
pub horizontal: Option<ScrollbarLayout>,
|
||
pub visible: bool,
|
||
}
|
||
|
||
impl EditorScrollbars {
|
||
pub fn from_scrollbar_axes(
|
||
show_scrollbar: ScrollbarAxes,
|
||
layout_information: &ScrollbarLayoutInformation,
|
||
content_offset: gpui::Point<Pixels>,
|
||
scroll_position: gpui::Point<f64>,
|
||
scrollbar_width: Pixels,
|
||
right_margin: Pixels,
|
||
editor_width: Pixels,
|
||
show_scrollbars: bool,
|
||
scrollbar_state: Option<&ActiveScrollbarState>,
|
||
window: &mut Window,
|
||
) -> Self {
|
||
let ScrollbarLayoutInformation {
|
||
editor_bounds,
|
||
scroll_range,
|
||
glyph_grid_cell,
|
||
} = layout_information;
|
||
|
||
let viewport_size = size(editor_width, editor_bounds.size.height);
|
||
|
||
let scrollbar_bounds_for = |axis: ScrollbarAxis| match axis {
|
||
ScrollbarAxis::Horizontal => Bounds::from_anchor_and_size(
|
||
gpui::Anchor::BottomLeft,
|
||
editor_bounds.bottom_left(),
|
||
size(
|
||
// The horizontal viewport size differs from the space available for the
|
||
// horizontal scrollbar, so we have to manually stitch it together here.
|
||
editor_bounds.size.width - right_margin,
|
||
scrollbar_width,
|
||
),
|
||
),
|
||
ScrollbarAxis::Vertical => Bounds::from_anchor_and_size(
|
||
gpui::Anchor::TopRight,
|
||
editor_bounds.top_right(),
|
||
size(scrollbar_width, viewport_size.height),
|
||
),
|
||
};
|
||
|
||
let mut create_scrollbar_layout = |axis| {
|
||
let viewport_size = viewport_size.along(axis);
|
||
let scroll_range = scroll_range.along(axis);
|
||
|
||
// We always want a vertical scrollbar track for scrollbar diagnostic visibility.
|
||
(show_scrollbar.along(axis)
|
||
&& (axis == ScrollbarAxis::Vertical || scroll_range > viewport_size))
|
||
.then(|| {
|
||
ScrollbarLayout::new(
|
||
window.insert_hitbox(scrollbar_bounds_for(axis), HitboxBehavior::Normal),
|
||
viewport_size,
|
||
scroll_range,
|
||
glyph_grid_cell.along(axis),
|
||
content_offset.along(axis),
|
||
scroll_position.along(axis),
|
||
show_scrollbars,
|
||
axis,
|
||
)
|
||
.with_thumb_state(
|
||
scrollbar_state.and_then(|state| state.thumb_state_for_axis(axis)),
|
||
)
|
||
})
|
||
};
|
||
|
||
Self {
|
||
vertical: create_scrollbar_layout(ScrollbarAxis::Vertical),
|
||
horizontal: create_scrollbar_layout(ScrollbarAxis::Horizontal),
|
||
visible: show_scrollbars,
|
||
}
|
||
}
|
||
|
||
pub fn iter_scrollbars(&self) -> impl Iterator<Item = (&ScrollbarLayout, ScrollbarAxis)> + '_ {
|
||
[
|
||
(&self.vertical, ScrollbarAxis::Vertical),
|
||
(&self.horizontal, ScrollbarAxis::Horizontal),
|
||
]
|
||
.into_iter()
|
||
.filter_map(|(scrollbar, axis)| scrollbar.as_ref().map(|s| (s, axis)))
|
||
}
|
||
|
||
/// Returns the currently hovered scrollbar axis, if any.
|
||
pub fn get_hovered_axis(&self, window: &Window) -> Option<(&ScrollbarLayout, ScrollbarAxis)> {
|
||
self.iter_scrollbars()
|
||
.find(|s| s.0.hitbox.is_hovered(window))
|
||
}
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
struct ScrollbarLayout {
|
||
hitbox: Hitbox,
|
||
visible_range: Range<ScrollOffset>,
|
||
text_unit_size: Pixels,
|
||
thumb_bounds: Option<Bounds<Pixels>>,
|
||
thumb_state: ScrollbarThumbState,
|
||
}
|
||
|
||
impl ScrollbarLayout {
|
||
const BORDER_WIDTH: Pixels = px(1.0);
|
||
const LINE_MARKER_HEIGHT: Pixels = px(2.0);
|
||
const MIN_MARKER_HEIGHT: Pixels = px(5.0);
|
||
const MIN_THUMB_SIZE: Pixels = px(25.0);
|
||
|
||
fn new(
|
||
scrollbar_track_hitbox: Hitbox,
|
||
viewport_size: Pixels,
|
||
scroll_range: Pixels,
|
||
glyph_space: Pixels,
|
||
content_offset: Pixels,
|
||
scroll_position: ScrollOffset,
|
||
show_thumb: bool,
|
||
axis: ScrollbarAxis,
|
||
) -> Self {
|
||
let track_bounds = scrollbar_track_hitbox.bounds;
|
||
// The length of the track available to the scrollbar thumb. We deliberately
|
||
// exclude the content size here so that the thumb aligns with the content.
|
||
let track_length = track_bounds.size.along(axis) - content_offset;
|
||
|
||
Self::new_with_hitbox_and_track_length(
|
||
scrollbar_track_hitbox,
|
||
track_length,
|
||
viewport_size,
|
||
scroll_range.into(),
|
||
glyph_space,
|
||
content_offset.into(),
|
||
scroll_position,
|
||
show_thumb,
|
||
axis,
|
||
)
|
||
}
|
||
|
||
fn for_minimap(
|
||
minimap_track_hitbox: Hitbox,
|
||
visible_lines: f64,
|
||
total_editor_lines: f64,
|
||
minimap_line_height: Pixels,
|
||
scroll_position: ScrollOffset,
|
||
minimap_scroll_top: ScrollOffset,
|
||
show_thumb: bool,
|
||
) -> Self {
|
||
// The scrollbar thumb size is calculated as
|
||
// (visible_content/total_content) × scrollbar_track_length.
|
||
//
|
||
// For the minimap's thumb layout, we leverage this by setting the
|
||
// scrollbar track length to the entire document size (using minimap line
|
||
// height). This creates a thumb that exactly represents the editor
|
||
// viewport scaled to minimap proportions.
|
||
//
|
||
// We adjust the thumb position relative to `minimap_scroll_top` to
|
||
// accommodate for the deliberately oversized track.
|
||
//
|
||
// This approach ensures that the minimap thumb accurately reflects the
|
||
// editor's current scroll position whilst nicely synchronizing the minimap
|
||
// thumb and scrollbar thumb.
|
||
let scroll_range = total_editor_lines * f64::from(minimap_line_height);
|
||
let viewport_size = visible_lines * f64::from(minimap_line_height);
|
||
|
||
let track_top_offset = -minimap_scroll_top * f64::from(minimap_line_height);
|
||
|
||
Self::new_with_hitbox_and_track_length(
|
||
minimap_track_hitbox,
|
||
Pixels::from(scroll_range),
|
||
Pixels::from(viewport_size),
|
||
scroll_range,
|
||
minimap_line_height,
|
||
track_top_offset,
|
||
scroll_position,
|
||
show_thumb,
|
||
ScrollbarAxis::Vertical,
|
||
)
|
||
}
|
||
|
||
fn new_with_hitbox_and_track_length(
|
||
scrollbar_track_hitbox: Hitbox,
|
||
track_length: Pixels,
|
||
viewport_size: Pixels,
|
||
scroll_range: f64,
|
||
glyph_space: Pixels,
|
||
content_offset: ScrollOffset,
|
||
scroll_position: ScrollOffset,
|
||
show_thumb: bool,
|
||
axis: ScrollbarAxis,
|
||
) -> Self {
|
||
let text_units_per_page = viewport_size.to_f64() / glyph_space.to_f64();
|
||
let visible_range = scroll_position..scroll_position + text_units_per_page;
|
||
let total_text_units = scroll_range / glyph_space.to_f64();
|
||
|
||
let thumb_percentage = text_units_per_page / total_text_units;
|
||
let thumb_size = Pixels::from(ScrollOffset::from(track_length) * thumb_percentage)
|
||
.max(ScrollbarLayout::MIN_THUMB_SIZE)
|
||
.min(track_length);
|
||
|
||
let text_unit_divisor = (total_text_units - text_units_per_page).max(0.);
|
||
|
||
let content_larger_than_viewport = text_unit_divisor > 0.;
|
||
|
||
let text_unit_size = if content_larger_than_viewport {
|
||
Pixels::from(ScrollOffset::from(track_length - thumb_size) / text_unit_divisor)
|
||
} else {
|
||
glyph_space
|
||
};
|
||
|
||
let thumb_bounds = (show_thumb && content_larger_than_viewport).then(|| {
|
||
Self::thumb_bounds(
|
||
&scrollbar_track_hitbox,
|
||
content_offset,
|
||
visible_range.start,
|
||
text_unit_size,
|
||
thumb_size,
|
||
axis,
|
||
)
|
||
});
|
||
|
||
ScrollbarLayout {
|
||
hitbox: scrollbar_track_hitbox,
|
||
visible_range,
|
||
text_unit_size,
|
||
thumb_bounds,
|
||
thumb_state: Default::default(),
|
||
}
|
||
}
|
||
|
||
fn with_thumb_state(self, thumb_state: Option<ScrollbarThumbState>) -> Self {
|
||
if let Some(thumb_state) = thumb_state {
|
||
Self {
|
||
thumb_state,
|
||
..self
|
||
}
|
||
} else {
|
||
self
|
||
}
|
||
}
|
||
|
||
fn thumb_bounds(
|
||
scrollbar_track: &Hitbox,
|
||
content_offset: f64,
|
||
visible_range_start: f64,
|
||
text_unit_size: Pixels,
|
||
thumb_size: Pixels,
|
||
axis: ScrollbarAxis,
|
||
) -> Bounds<Pixels> {
|
||
let thumb_origin = scrollbar_track.origin.apply_along(axis, |origin| {
|
||
origin
|
||
+ Pixels::from(
|
||
content_offset + visible_range_start * ScrollOffset::from(text_unit_size),
|
||
)
|
||
});
|
||
Bounds::new(
|
||
thumb_origin,
|
||
scrollbar_track.size.apply_along(axis, |_| thumb_size),
|
||
)
|
||
}
|
||
|
||
fn thumb_hovered(&self, position: &gpui::Point<Pixels>) -> bool {
|
||
self.thumb_bounds
|
||
.is_some_and(|bounds| bounds.contains(position))
|
||
}
|
||
|
||
fn marker_quads_for_ranges(
|
||
&self,
|
||
row_ranges: impl IntoIterator<Item = ColoredRange<DisplayRow>>,
|
||
column: Option<usize>,
|
||
) -> Vec<PaintQuad> {
|
||
struct MinMax {
|
||
min: Pixels,
|
||
max: Pixels,
|
||
}
|
||
let (x_range, height_limit) = if let Some(column) = column {
|
||
let column_width = ((self.hitbox.size.width - Self::BORDER_WIDTH) / 3.0).floor();
|
||
let start = Self::BORDER_WIDTH + (column as f32 * column_width);
|
||
let end = start + column_width;
|
||
(
|
||
Range { start, end },
|
||
MinMax {
|
||
min: Self::MIN_MARKER_HEIGHT,
|
||
max: px(f32::MAX),
|
||
},
|
||
)
|
||
} else {
|
||
(
|
||
Range {
|
||
start: Self::BORDER_WIDTH,
|
||
end: self.hitbox.size.width,
|
||
},
|
||
MinMax {
|
||
min: Self::LINE_MARKER_HEIGHT,
|
||
max: Self::LINE_MARKER_HEIGHT,
|
||
},
|
||
)
|
||
};
|
||
|
||
let row_to_y = |row: DisplayRow| row.as_f64() as f32 * self.text_unit_size;
|
||
let mut pixel_ranges = row_ranges
|
||
.into_iter()
|
||
.map(|range| {
|
||
let start_y = row_to_y(range.start);
|
||
let end_y = row_to_y(range.end)
|
||
+ self
|
||
.text_unit_size
|
||
.max(height_limit.min)
|
||
.min(height_limit.max);
|
||
ColoredRange {
|
||
start: start_y,
|
||
end: end_y,
|
||
color: range.color,
|
||
}
|
||
})
|
||
.peekable();
|
||
|
||
let mut quads = Vec::new();
|
||
while let Some(mut pixel_range) = pixel_ranges.next() {
|
||
while let Some(next_pixel_range) = pixel_ranges.peek() {
|
||
if pixel_range.end >= next_pixel_range.start - px(1.0)
|
||
&& pixel_range.color == next_pixel_range.color
|
||
{
|
||
pixel_range.end = next_pixel_range.end.max(pixel_range.end);
|
||
pixel_ranges.next();
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
let bounds = Bounds::from_corners(
|
||
point(x_range.start, pixel_range.start),
|
||
point(x_range.end, pixel_range.end),
|
||
);
|
||
quads.push(quad(
|
||
bounds,
|
||
Corners::default(),
|
||
pixel_range.color,
|
||
Edges::default(),
|
||
Hsla::transparent_black(),
|
||
BorderStyle::default(),
|
||
));
|
||
}
|
||
|
||
quads
|
||
}
|
||
}
|
||
|
||
struct MinimapLayout {
|
||
pub minimap: AnyElement,
|
||
pub thumb_layout: ScrollbarLayout,
|
||
pub minimap_scroll_top: ScrollOffset,
|
||
pub minimap_line_height: Pixels,
|
||
pub thumb_border_style: MinimapThumbBorder,
|
||
pub max_scroll_top: ScrollOffset,
|
||
}
|
||
|
||
impl MinimapLayout {
|
||
/// The minimum width of the minimap in columns. If the minimap is smaller than this, it will be hidden.
|
||
const MINIMAP_MIN_WIDTH_COLUMNS: f32 = 20.;
|
||
/// The minimap width as a percentage of the editor width.
|
||
const MINIMAP_WIDTH_PCT: f32 = 0.15;
|
||
/// Calculates the scroll top offset the minimap editor has to have based on the
|
||
/// current scroll progress.
|
||
fn calculate_minimap_top_offset(
|
||
document_lines: f64,
|
||
visible_editor_lines: f64,
|
||
visible_minimap_lines: f64,
|
||
scroll_position: f64,
|
||
) -> ScrollOffset {
|
||
let non_visible_document_lines = (document_lines - visible_editor_lines).max(0.);
|
||
if non_visible_document_lines == 0. {
|
||
0.
|
||
} else {
|
||
let scroll_percentage = (scroll_position / non_visible_document_lines).clamp(0., 1.);
|
||
scroll_percentage * (document_lines - visible_minimap_lines).max(0.)
|
||
}
|
||
}
|
||
}
|
||
|
||
struct CreaseTrailerLayout {
|
||
element: AnyElement,
|
||
bounds: Bounds<Pixels>,
|
||
}
|
||
|
||
pub(crate) struct PositionMap {
|
||
pub size: Size<Pixels>,
|
||
pub line_height: Pixels,
|
||
pub scroll_position: gpui::Point<ScrollOffset>,
|
||
pub scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
pub scroll_max: gpui::Point<ScrollOffset>,
|
||
pub em_advance: Pixels,
|
||
pub em_layout_width: Pixels,
|
||
pub visible_row_range: Range<DisplayRow>,
|
||
pub line_layouts: Vec<LineWithInvisibles>,
|
||
pub snapshot: EditorSnapshot,
|
||
pub text_align: TextAlign,
|
||
pub content_width: Pixels,
|
||
pub text_hitbox: Hitbox,
|
||
pub gutter_hitbox: Hitbox,
|
||
pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,
|
||
pub display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||
pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds<Pixels>)>,
|
||
}
|
||
|
||
#[derive(Debug, Copy, Clone)]
|
||
pub struct PointForPosition {
|
||
pub previous_valid: DisplayPoint,
|
||
pub next_valid: DisplayPoint,
|
||
pub nearest_valid: DisplayPoint,
|
||
pub exact_unclipped: DisplayPoint,
|
||
pub column_overshoot_after_line_end: u32,
|
||
}
|
||
|
||
impl PointForPosition {
|
||
pub fn as_valid(&self) -> Option<DisplayPoint> {
|
||
if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
|
||
Some(self.previous_valid)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
pub fn intersects_selection(&self, selection: &Selection<DisplayPoint>) -> bool {
|
||
let Some(valid_point) = self.as_valid() else {
|
||
return false;
|
||
};
|
||
let range = selection.range();
|
||
|
||
let candidate_row = valid_point.row();
|
||
let candidate_col = valid_point.column();
|
||
|
||
let start_row = range.start.row();
|
||
let start_col = range.start.column();
|
||
let end_row = range.end.row();
|
||
let end_col = range.end.column();
|
||
|
||
if candidate_row < start_row || candidate_row > end_row {
|
||
false
|
||
} else if start_row == end_row {
|
||
candidate_col >= start_col && candidate_col < end_col
|
||
} else if candidate_row == start_row {
|
||
candidate_col >= start_col
|
||
} else if candidate_row == end_row {
|
||
candidate_col < end_col
|
||
} else {
|
||
true
|
||
}
|
||
}
|
||
}
|
||
|
||
impl PositionMap {
|
||
pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
|
||
let text_bounds = self.text_hitbox.bounds;
|
||
let scroll_position = self.scroll_position;
|
||
let position = position - text_bounds.origin;
|
||
let y = position.y.max(px(0.)).min(self.size.height);
|
||
let x = position.x + (scroll_position.x as f32 * self.em_layout_width);
|
||
let row = ((y / self.line_height) as f64 + scroll_position.y) as u32;
|
||
|
||
let (column, x_overshoot_after_line_end) = if let Some(line_index) =
|
||
row.checked_sub(self.visible_row_range.start.0)
|
||
&& let Some(line) = self.line_layouts.get(line_index as usize)
|
||
{
|
||
let alignment_offset = line.alignment_offset(self.text_align, self.content_width);
|
||
let x_relative_to_text = x - alignment_offset;
|
||
if let Some(ix) = line.index_for_x(x_relative_to_text) {
|
||
(ix as u32, px(0.))
|
||
} else {
|
||
(line.len as u32, px(0.).max(x_relative_to_text - line.width))
|
||
}
|
||
} else {
|
||
(0, x)
|
||
};
|
||
|
||
let mut exact_unclipped = DisplayPoint::new(DisplayRow(row), column);
|
||
let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
|
||
let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
|
||
|
||
let nearest_valid = if previous_valid == next_valid {
|
||
previous_valid
|
||
} else {
|
||
match self.snapshot.inlay_bias_at(exact_unclipped) {
|
||
Some(Bias::Left) => next_valid,
|
||
Some(Bias::Right) => previous_valid,
|
||
None => previous_valid,
|
||
}
|
||
};
|
||
|
||
let column_overshoot_after_line_end =
|
||
(x_overshoot_after_line_end / self.em_layout_width) as u32;
|
||
*exact_unclipped.column_mut() += column_overshoot_after_line_end;
|
||
PointForPosition {
|
||
previous_valid,
|
||
next_valid,
|
||
nearest_valid,
|
||
exact_unclipped,
|
||
column_overshoot_after_line_end,
|
||
}
|
||
}
|
||
|
||
fn point_for_position_on_line(
|
||
&self,
|
||
position: gpui::Point<Pixels>,
|
||
row: DisplayRow,
|
||
line: &LineWithInvisibles,
|
||
) -> PointForPosition {
|
||
let text_bounds = self.text_hitbox.bounds;
|
||
let scroll_position = self.scroll_position;
|
||
let position = position - text_bounds.origin;
|
||
let x = position.x + (scroll_position.x as f32 * self.em_layout_width);
|
||
|
||
let alignment_offset = line.alignment_offset(self.text_align, self.content_width);
|
||
let x_relative_to_text = x - alignment_offset;
|
||
let (column, x_overshoot_after_line_end) =
|
||
if let Some(ix) = line.index_for_x(x_relative_to_text) {
|
||
(ix as u32, px(0.))
|
||
} else {
|
||
(line.len as u32, px(0.).max(x_relative_to_text - line.width))
|
||
};
|
||
|
||
let mut exact_unclipped = DisplayPoint::new(row, column);
|
||
let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
|
||
let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
|
||
|
||
let nearest_valid = if previous_valid == next_valid {
|
||
previous_valid
|
||
} else {
|
||
match self.snapshot.inlay_bias_at(exact_unclipped) {
|
||
Some(Bias::Left) => next_valid,
|
||
Some(Bias::Right) => previous_valid,
|
||
None => previous_valid,
|
||
}
|
||
};
|
||
|
||
let column_overshoot_after_line_end =
|
||
(x_overshoot_after_line_end / self.em_layout_width) as u32;
|
||
*exact_unclipped.column_mut() += column_overshoot_after_line_end;
|
||
PointForPosition {
|
||
previous_valid,
|
||
next_valid,
|
||
nearest_valid,
|
||
exact_unclipped,
|
||
column_overshoot_after_line_end,
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) struct BlockLayout {
|
||
pub(crate) id: BlockId,
|
||
pub(crate) x_offset: Pixels,
|
||
pub(crate) row: Option<DisplayRow>,
|
||
pub(crate) element: AnyElement,
|
||
pub(crate) available_space: Size<AvailableSpace>,
|
||
pub(crate) style: BlockStyle,
|
||
pub(crate) overlaps_gutter: bool,
|
||
pub(crate) is_buffer_header: bool,
|
||
}
|
||
|
||
pub fn layout_line(
|
||
row: DisplayRow,
|
||
snapshot: &EditorSnapshot,
|
||
style: &EditorStyle,
|
||
text_width: Pixels,
|
||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) -> LineWithInvisibles {
|
||
let use_tree_sitter =
|
||
!snapshot.semantic_tokens_enabled || snapshot.use_tree_sitter_for_syntax(row, cx);
|
||
let language_aware = LanguageAwareStyling {
|
||
tree_sitter: use_tree_sitter,
|
||
diagnostics: true,
|
||
};
|
||
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), language_aware, style);
|
||
LineWithInvisibles::from_chunks(
|
||
chunks,
|
||
style,
|
||
MAX_LINE_LEN,
|
||
1,
|
||
&snapshot.mode,
|
||
text_width,
|
||
is_row_soft_wrapped,
|
||
&[],
|
||
window,
|
||
cx,
|
||
)
|
||
.pop()
|
||
.unwrap()
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct IndentGuideLayout {
|
||
origin: gpui::Point<Pixels>,
|
||
length: Pixels,
|
||
single_indent_width: Pixels,
|
||
display_row_range: Range<DisplayRow>,
|
||
depth: u32,
|
||
active: bool,
|
||
settings: IndentGuideSettings,
|
||
}
|
||
|
||
enum NavigationOverlayPaintCommand {
|
||
Label(NavigationLabelLayout),
|
||
}
|
||
|
||
struct NavigationLabelLayout {
|
||
element: AnyElement,
|
||
#[cfg_attr(not(test), allow(dead_code))]
|
||
origin: gpui::Point<Pixels>,
|
||
}
|
||
|
||
struct NavigationOverlayLayoutContext<'a> {
|
||
display_snapshot: &'a DisplaySnapshot,
|
||
visible_display_row_range: &'a Range<DisplayRow>,
|
||
line_layouts: &'a [LineWithInvisibles],
|
||
text_align: TextAlign,
|
||
content_width: Pixels,
|
||
content_origin: gpui::Point<Pixels>,
|
||
scroll_position: gpui::Point<ScrollOffset>,
|
||
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
|
||
line_height: Pixels,
|
||
editor_font: Font,
|
||
editor_font_size: Pixels,
|
||
}
|
||
|
||
const LABEL_LINE_HEIGHT_PADDING_PX: f32 = 2.0;
|
||
|
||
pub struct CursorLayout {
|
||
origin: gpui::Point<Pixels>,
|
||
block_width: Pixels,
|
||
line_height: Pixels,
|
||
color: Hsla,
|
||
shape: CursorShape,
|
||
block_text: Option<ShapedLine>,
|
||
cursor_name: Option<AnyElement>,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct CursorName {
|
||
string: SharedString,
|
||
color: Hsla,
|
||
is_top_row: bool,
|
||
}
|
||
|
||
impl CursorLayout {
|
||
pub fn new(
|
||
origin: gpui::Point<Pixels>,
|
||
block_width: Pixels,
|
||
line_height: Pixels,
|
||
color: Hsla,
|
||
shape: CursorShape,
|
||
block_text: Option<ShapedLine>,
|
||
) -> CursorLayout {
|
||
CursorLayout {
|
||
origin,
|
||
block_width,
|
||
line_height,
|
||
color,
|
||
shape,
|
||
block_text,
|
||
cursor_name: None,
|
||
}
|
||
}
|
||
|
||
pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
|
||
Bounds {
|
||
origin: self.origin + origin,
|
||
size: size(self.block_width, self.line_height),
|
||
}
|
||
}
|
||
|
||
fn bounds(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
|
||
match self.shape {
|
||
CursorShape::Bar => Bounds {
|
||
origin: self.origin + origin,
|
||
size: size(px(2.0), self.line_height),
|
||
},
|
||
CursorShape::Block | CursorShape::Hollow => Bounds {
|
||
origin: self.origin + origin,
|
||
size: size(self.block_width, self.line_height),
|
||
},
|
||
CursorShape::Underline => Bounds {
|
||
origin: self.origin
|
||
+ origin
|
||
+ gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)),
|
||
size: size(self.block_width, px(2.0)),
|
||
},
|
||
}
|
||
}
|
||
|
||
pub fn layout(
|
||
&mut self,
|
||
origin: gpui::Point<Pixels>,
|
||
cursor_name: Option<CursorName>,
|
||
window: &mut Window,
|
||
cx: &mut App,
|
||
) {
|
||
if let Some(cursor_name) = cursor_name {
|
||
let bounds = self.bounds(origin);
|
||
let text_size = self.line_height / 1.5;
|
||
|
||
let name_origin = if cursor_name.is_top_row {
|
||
point(bounds.right() - px(1.), bounds.top())
|
||
} else {
|
||
match self.shape {
|
||
CursorShape::Bar => point(
|
||
bounds.right() - px(2.),
|
||
bounds.top() - text_size / 2. - px(1.),
|
||
),
|
||
_ => point(
|
||
bounds.right() - px(1.),
|
||
bounds.top() - text_size / 2. - px(1.),
|
||
),
|
||
}
|
||
};
|
||
let mut name_element = div()
|
||
.bg(self.color)
|
||
.text_size(text_size)
|
||
.px_0p5()
|
||
.line_height(text_size + px(LABEL_LINE_HEIGHT_PADDING_PX))
|
||
.text_color(cursor_name.color)
|
||
.child(cursor_name.string)
|
||
.into_any_element();
|
||
|
||
name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx);
|
||
|
||
self.cursor_name = Some(name_element);
|
||
}
|
||
}
|
||
|
||
pub fn paint(&mut self, origin: gpui::Point<Pixels>, window: &mut Window, cx: &mut App) {
|
||
let bounds = window.pixel_snap_bounds(self.bounds(origin));
|
||
|
||
//Draw background or border quad
|
||
let cursor = if matches!(self.shape, CursorShape::Hollow) {
|
||
outline(bounds, self.color, BorderStyle::Solid)
|
||
} else {
|
||
fill(bounds, self.color)
|
||
};
|
||
|
||
if let Some(name) = &mut self.cursor_name {
|
||
name.paint(window, cx);
|
||
}
|
||
|
||
window.paint_quad(cursor);
|
||
|
||
if let Some(block_text) = &self.block_text {
|
||
block_text
|
||
.paint(
|
||
self.origin + origin,
|
||
self.line_height,
|
||
TextAlign::Left,
|
||
None,
|
||
window,
|
||
cx,
|
||
)
|
||
.log_err();
|
||
}
|
||
}
|
||
|
||
pub fn shape(&self) -> CursorShape {
|
||
self.shape
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct HighlightedRange {
|
||
pub start_y: Pixels,
|
||
pub line_height: Pixels,
|
||
pub lines: Vec<HighlightedRangeLine>,
|
||
pub color: Hsla,
|
||
pub corner_radius: Pixels,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct HighlightedRangeLine {
|
||
pub start_x: Pixels,
|
||
pub end_x: Pixels,
|
||
}
|
||
|
||
impl HighlightedRange {
|
||
pub fn paint(&self, fill: bool, bounds: Bounds<Pixels>, window: &mut Window) {
|
||
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
|
||
self.paint_lines(self.start_y, &self.lines[0..1], fill, bounds, window);
|
||
self.paint_lines(
|
||
self.start_y + self.line_height,
|
||
&self.lines[1..],
|
||
fill,
|
||
bounds,
|
||
window,
|
||
);
|
||
} else {
|
||
self.paint_lines(self.start_y, &self.lines, fill, bounds, window);
|
||
}
|
||
}
|
||
|
||
fn paint_lines(
|
||
&self,
|
||
start_y: Pixels,
|
||
lines: &[HighlightedRangeLine],
|
||
fill: bool,
|
||
_bounds: Bounds<Pixels>,
|
||
window: &mut Window,
|
||
) {
|
||
if lines.is_empty() {
|
||
return;
|
||
}
|
||
|
||
let first_line = lines.first().unwrap();
|
||
let last_line = lines.last().unwrap();
|
||
|
||
let first_top_left = point(first_line.start_x, start_y);
|
||
let first_top_right = point(first_line.end_x, start_y);
|
||
|
||
let curve_height = point(Pixels::ZERO, self.corner_radius);
|
||
let curve_width = |start_x: Pixels, end_x: Pixels| {
|
||
let max = (end_x - start_x) / 2.;
|
||
let width = if max < self.corner_radius {
|
||
max
|
||
} else {
|
||
self.corner_radius
|
||
};
|
||
|
||
point(width, Pixels::ZERO)
|
||
};
|
||
|
||
let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
|
||
let mut builder = if fill {
|
||
gpui::PathBuilder::fill()
|
||
} else {
|
||
gpui::PathBuilder::stroke(px(1.))
|
||
};
|
||
builder.move_to(first_top_right - top_curve_width);
|
||
builder.curve_to(first_top_right + curve_height, first_top_right);
|
||
|
||
let mut iter = lines.iter().enumerate().peekable();
|
||
while let Some((ix, line)) = iter.next() {
|
||
let bottom_right = point(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
|
||
|
||
if let Some((_, next_line)) = iter.peek() {
|
||
let next_top_right = point(next_line.end_x, bottom_right.y);
|
||
|
||
match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() {
|
||
Ordering::Equal => {
|
||
builder.line_to(bottom_right);
|
||
}
|
||
Ordering::Less => {
|
||
let curve_width = curve_width(next_top_right.x, bottom_right.x);
|
||
builder.line_to(bottom_right - curve_height);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(bottom_right - curve_width, bottom_right);
|
||
}
|
||
builder.line_to(next_top_right + curve_width);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(next_top_right + curve_height, next_top_right);
|
||
}
|
||
}
|
||
Ordering::Greater => {
|
||
let curve_width = curve_width(bottom_right.x, next_top_right.x);
|
||
builder.line_to(bottom_right - curve_height);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(bottom_right + curve_width, bottom_right);
|
||
}
|
||
builder.line_to(next_top_right - curve_width);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(next_top_right + curve_height, next_top_right);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
let curve_width = curve_width(line.start_x, line.end_x);
|
||
builder.line_to(bottom_right - curve_height);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(bottom_right - curve_width, bottom_right);
|
||
}
|
||
|
||
let bottom_left = point(line.start_x, bottom_right.y);
|
||
builder.line_to(bottom_left + curve_width);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(bottom_left - curve_height, bottom_left);
|
||
}
|
||
}
|
||
}
|
||
|
||
if first_line.start_x > last_line.start_x {
|
||
let curve_width = curve_width(last_line.start_x, first_line.start_x);
|
||
let second_top_left = point(last_line.start_x, start_y + self.line_height);
|
||
builder.line_to(second_top_left + curve_height);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(second_top_left + curve_width, second_top_left);
|
||
}
|
||
let first_bottom_left = point(first_line.start_x, second_top_left.y);
|
||
builder.line_to(first_bottom_left - curve_width);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(first_bottom_left - curve_height, first_bottom_left);
|
||
}
|
||
}
|
||
|
||
builder.line_to(first_top_left + curve_height);
|
||
if self.corner_radius > Pixels::ZERO {
|
||
builder.curve_to(first_top_left + top_curve_width, first_top_left);
|
||
}
|
||
builder.line_to(first_top_right - top_curve_width);
|
||
|
||
if let Ok(path) = builder.build() {
|
||
window.paint_path(path, self.color);
|
||
}
|
||
}
|
||
}
|
||
|
||
enum CursorPopoverType {
|
||
CodeContextMenu,
|
||
EditPrediction,
|
||
}
|
||
|
||
pub fn register_action<T: Action>(
|
||
editor: &Entity<Editor>,
|
||
window: &mut Window,
|
||
listener: impl Fn(&mut Editor, &T, &mut Window, &mut Context<Editor>) + 'static,
|
||
) {
|
||
let editor = editor.clone();
|
||
window.on_action(TypeId::of::<T>(), move |action, phase, window, cx| {
|
||
let action = action.downcast_ref().unwrap();
|
||
if phase == DispatchPhase::Bubble {
|
||
editor.update(cx, |editor, cx| {
|
||
listener(editor, action, window, cx);
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Shared between `prepaint` and `compute_auto_height_layout` to ensure
|
||
/// both full and auto-height editors compute wrap widths consistently.
|
||
fn calculate_wrap_width(
|
||
soft_wrap: SoftWrap,
|
||
editor_width: Pixels,
|
||
em_width: Pixels,
|
||
) -> Option<Pixels> {
|
||
let wrap_width_for = |column: u32| (column as f32 * em_width).ceil();
|
||
|
||
match soft_wrap {
|
||
SoftWrap::GitDiff => None,
|
||
SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
|
||
SoftWrap::EditorWidth => Some(editor_width),
|
||
SoftWrap::Bounded(column) => Some(editor_width.min(wrap_width_for(column))),
|
||
}
|
||
}
|
||
|
||
fn compute_auto_height_layout(
|
||
editor: &mut Editor,
|
||
min_lines: usize,
|
||
max_lines: Option<usize>,
|
||
known_dimensions: Size<Option<Pixels>>,
|
||
available_width: AvailableSpace,
|
||
window: &mut Window,
|
||
cx: &mut Context<Editor>,
|
||
) -> Option<Size<Pixels>> {
|
||
let width = known_dimensions.width.or({
|
||
if let AvailableSpace::Definite(available_width) = available_width {
|
||
Some(available_width)
|
||
} else {
|
||
None
|
||
}
|
||
})?;
|
||
if let Some(height) = known_dimensions.height {
|
||
return Some(size(width, height));
|
||
}
|
||
|
||
let style = editor.style.as_ref().unwrap();
|
||
let font_id = window.text_system().resolve_font(&style.text.font());
|
||
let font_size = style.text.font_size.to_pixels(window.rem_size());
|
||
let line_height = style.text.line_height_in_pixels(window.rem_size());
|
||
let em_width = window.text_system().em_width(font_id, font_size).unwrap();
|
||
|
||
let mut snapshot = editor.snapshot(window, cx);
|
||
let gutter_dimensions = snapshot.gutter_dimensions(font_id, font_size, style, window, cx);
|
||
|
||
editor.gutter_dimensions = gutter_dimensions;
|
||
let text_width = width - gutter_dimensions.width;
|
||
let overscroll = size(em_width, px(0.));
|
||
|
||
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
|
||
let wrap_width = calculate_wrap_width(editor.soft_wrap_mode(cx), editor_width, em_width)
|
||
.map(|width| width.min(editor_width));
|
||
if wrap_width.is_some() && editor.set_wrap_width(wrap_width, cx) {
|
||
snapshot = editor.snapshot(window, cx);
|
||
}
|
||
|
||
let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
|
||
|
||
let min_height = line_height * min_lines as f32;
|
||
let content_height = scroll_height.max(min_height);
|
||
|
||
let final_height = if let Some(max_lines) = max_lines {
|
||
let max_height = line_height * max_lines as f32;
|
||
content_height.min(max_height)
|
||
} else {
|
||
content_height
|
||
};
|
||
|
||
Some(size(width, final_height))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::{
|
||
Editor, HighlightKey, MultiBuffer, NavigationOverlayKey, NavigationOverlayLabel,
|
||
NavigationTargetOverlay, SelectionEffects,
|
||
display_map::{BlockPlacement, BlockProperties},
|
||
editor_tests::{init_test, update_test_language_settings},
|
||
};
|
||
use gpui::{TestAppContext, VisualTestContext};
|
||
use language::{Buffer, language_settings, tree_sitter_python};
|
||
use log::info;
|
||
use rand::{RngCore, rngs::StdRng};
|
||
use std::num::NonZeroU32;
|
||
use util::test::sample_text;
|
||
|
||
enum PrimaryNavigationOverlay {}
|
||
|
||
const PRIMARY_NAVIGATION_OVERLAY_KEY: NavigationOverlayKey =
|
||
NavigationOverlayKey::unique::<PrimaryNavigationOverlay>();
|
||
|
||
fn navigation_overlay(
|
||
label_text: &'static str,
|
||
target_range: Range<Anchor>,
|
||
covered_text_range: Option<Range<Anchor>>,
|
||
) -> NavigationTargetOverlay {
|
||
NavigationTargetOverlay {
|
||
target_range,
|
||
label: NavigationOverlayLabel {
|
||
text: SharedString::from(label_text),
|
||
text_color: Hsla::black(),
|
||
x_offset: Pixels::ZERO,
|
||
scale_factor: 1.0,
|
||
},
|
||
covered_text_range,
|
||
}
|
||
}
|
||
|
||
fn navigation_label_layouts(state: &EditorLayout) -> Vec<&NavigationLabelLayout> {
|
||
state
|
||
.navigation_overlay_paint_commands
|
||
.iter()
|
||
.map(|command| match command {
|
||
NavigationOverlayPaintCommand::Label(label) => label,
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
const fn placeholder_hitbox() -> Hitbox {
|
||
use gpui::HitboxId;
|
||
let zero_bounds = Bounds {
|
||
origin: point(Pixels::ZERO, Pixels::ZERO),
|
||
size: Size {
|
||
width: Pixels::ZERO,
|
||
height: Pixels::ZERO,
|
||
},
|
||
};
|
||
|
||
Hitbox {
|
||
id: HitboxId::placeholder(),
|
||
bounds: zero_bounds,
|
||
content_mask: ContentMask {
|
||
bounds: zero_bounds,
|
||
},
|
||
behavior: HitboxBehavior::Normal,
|
||
}
|
||
}
|
||
|
||
fn test_gutter(line_height: Pixels, snapshot: &EditorSnapshot) -> Gutter<'_> {
|
||
const DIMENSIONS: GutterDimensions = GutterDimensions {
|
||
left_padding: Pixels::ZERO,
|
||
right_padding: Pixels::ZERO,
|
||
width: px(30.0),
|
||
margin: Pixels::ZERO,
|
||
git_blame_entries_width: None,
|
||
};
|
||
const EMPTY_ROW_INFO: RowInfo = RowInfo {
|
||
buffer_id: None,
|
||
buffer_row: None,
|
||
multibuffer_row: None,
|
||
diff_status: None,
|
||
expand_info: None,
|
||
wrapped_buffer_row: None,
|
||
};
|
||
|
||
const fn row_info(row: u32) -> RowInfo {
|
||
RowInfo {
|
||
buffer_row: Some(row),
|
||
..EMPTY_ROW_INFO
|
||
}
|
||
}
|
||
|
||
const ROW_INFOS: [RowInfo; 6] = [
|
||
row_info(0),
|
||
row_info(1),
|
||
row_info(2),
|
||
row_info(3),
|
||
row_info(4),
|
||
row_info(5),
|
||
];
|
||
|
||
const HITBOX: Hitbox = placeholder_hitbox();
|
||
Gutter {
|
||
line_height,
|
||
range: DisplayRow(0)..DisplayRow(6),
|
||
scroll_position: gpui::Point::default(),
|
||
dimensions: &DIMENSIONS,
|
||
hitbox: &HITBOX,
|
||
snapshot: snapshot,
|
||
row_infos: &ROW_INFOS,
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
|
||
let mut editor = Editor::new(
|
||
EditorMode::AutoHeight {
|
||
min_lines: 1,
|
||
max_lines: None,
|
||
},
|
||
buffer,
|
||
None,
|
||
window,
|
||
cx,
|
||
);
|
||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||
editor
|
||
});
|
||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||
let editor = window.root(cx).unwrap();
|
||
let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
|
||
|
||
for x in 1..=100 {
|
||
let (_, state) = cx.draw(
|
||
Default::default(),
|
||
size(px(200. + 0.13 * x as f32), px(500.)),
|
||
|_, _| EditorElement::new(&editor, style.clone()),
|
||
);
|
||
|
||
assert!(
|
||
state.position_map.scroll_max.x == 0.,
|
||
"Soft wrapped editor should have no horizontal scrolling!"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
|
||
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
|
||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||
editor
|
||
});
|
||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||
let editor = window.root(cx).unwrap();
|
||
let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
|
||
|
||
for x in 1..=100 {
|
||
let (_, state) = cx.draw(
|
||
Default::default(),
|
||
size(px(200. + 0.13 * x as f32), px(500.)),
|
||
|_, _| EditorElement::new(&editor, style.clone()),
|
||
);
|
||
|
||
assert!(
|
||
state.position_map.scroll_max.x == 0.,
|
||
"Soft wrapped editor should have no horizontal scrolling!"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_point_for_position_clipped_rows(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let text = "aaa\nbbb";
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple(text, cx);
|
||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||
});
|
||
|
||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||
let editor = window.root(cx).unwrap();
|
||
let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
|
||
let line_height = window
|
||
.update(cx, |_, window, _| {
|
||
style.text.line_height_in_pixels(window.rem_size())
|
||
})
|
||
.unwrap();
|
||
|
||
// the first line is clipped
|
||
let (_, state) = cx.draw(
|
||
point(Pixels::ZERO, Pixels::ZERO - line_height * 1.5),
|
||
size(px(500.), px(500.)),
|
||
|_, _| EditorElement::new(&editor, style),
|
||
);
|
||
|
||
// click at the end of the second line
|
||
let target_point = DisplayPoint::new(DisplayRow(1), 3);
|
||
let click_x = state.content_origin.x
|
||
+ editor.update_in(cx, |editor, window, cx| {
|
||
editor
|
||
.snapshot(window, cx)
|
||
.x_for_display_point(target_point, &editor.text_layout_details(window, cx))
|
||
});
|
||
|
||
let point = state
|
||
.position_map
|
||
.point_for_position(point(click_x, px(0.)));
|
||
assert_eq!(point.nearest_valid, target_point);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_navigation_overlay_covered_text_highlights_are_replaced(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple("overlay replacement", cx);
|
||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||
});
|
||
let editor = window.root(cx).unwrap();
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
||
let target_start = buffer_snapshot.anchor_after(Point::new(0, 0));
|
||
let target_end = buffer_snapshot.anchor_after(Point::new(0, 7));
|
||
let covered_text_end = buffer_snapshot.anchor_after(Point::new(0, 2));
|
||
|
||
editor.set_navigation_overlays(
|
||
PRIMARY_NAVIGATION_OVERLAY_KEY,
|
||
vec![navigation_overlay(
|
||
"ov",
|
||
target_start..target_end,
|
||
Some(target_start..covered_text_end),
|
||
)],
|
||
cx,
|
||
);
|
||
assert!(
|
||
editor
|
||
.text_highlights(
|
||
HighlightKey::NavigationOverlay(PRIMARY_NAVIGATION_OVERLAY_KEY),
|
||
cx,
|
||
)
|
||
.is_some()
|
||
);
|
||
|
||
editor.set_navigation_overlays(
|
||
PRIMARY_NAVIGATION_OVERLAY_KEY,
|
||
vec![navigation_overlay("ov", target_start..target_end, None)],
|
||
cx,
|
||
);
|
||
assert!(
|
||
editor
|
||
.text_highlights(
|
||
HighlightKey::NavigationOverlay(PRIMARY_NAVIGATION_OVERLAY_KEY),
|
||
cx,
|
||
)
|
||
.is_none()
|
||
);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_navigation_overlay_repositions_when_editor_width_changes(
|
||
cx: &mut TestAppContext,
|
||
) {
|
||
init_test(cx, |_| {});
|
||
let text = "jump target overlay ".repeat(16);
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple(&text, cx);
|
||
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
|
||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||
editor
|
||
});
|
||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||
let editor = window.root(cx).unwrap();
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
||
let target_start = buffer_snapshot.anchor_after(Point::new(0, 30));
|
||
let target_end = buffer_snapshot.anchor_after(Point::new(0, 40));
|
||
|
||
editor.set_navigation_overlays(
|
||
PRIMARY_NAVIGATION_OVERLAY_KEY,
|
||
vec![navigation_overlay("jj", target_start..target_end, None)],
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
|
||
let (_, wide_state) = cx.draw(Default::default(), size(px(520.), px(260.)), |_, _| {
|
||
EditorElement::new(&editor, style.clone())
|
||
});
|
||
let (_, narrow_state) = cx.draw(Default::default(), size(px(140.), px(260.)), |_, _| {
|
||
EditorElement::new(&editor, style.clone())
|
||
});
|
||
|
||
let wide_label_layouts = navigation_label_layouts(&wide_state);
|
||
let narrow_label_layouts = navigation_label_layouts(&narrow_state);
|
||
|
||
assert_eq!(wide_label_layouts.len(), 1);
|
||
assert_eq!(narrow_label_layouts.len(), 1);
|
||
|
||
let wide_label_origin = wide_label_layouts[0].origin;
|
||
let narrow_label_origin = narrow_label_layouts[0].origin;
|
||
|
||
assert!(
|
||
narrow_label_origin.y > wide_label_origin.y,
|
||
"expected inline label to move to a later wrapped row when the editor narrows"
|
||
);
|
||
assert!(
|
||
narrow_label_origin.x < wide_label_origin.x,
|
||
"expected inline label to recompute its horizontal position for the wrapped row"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_layout_line_numbers(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
|
||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||
});
|
||
|
||
let editor = window.root(cx).unwrap();
|
||
let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
|
||
let line_height = window
|
||
.update(cx, |_, window, _| {
|
||
style.text.line_height_in_pixels(window.rem_size())
|
||
})
|
||
.unwrap();
|
||
let element = EditorElement::new(&editor, style);
|
||
let snapshot = window
|
||
.update(cx, |editor, window, cx| editor.snapshot(window, cx))
|
||
.unwrap();
|
||
|
||
let layouts = cx
|
||
.update_window(*window, |_, window, cx| {
|
||
element.layout_line_numbers(
|
||
&test_gutter(line_height, &snapshot),
|
||
&BTreeMap::default(),
|
||
Some(DisplayRow(0)),
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert_eq!(layouts.len(), 6);
|
||
|
||
let relative_rows = window
|
||
.update(cx, |editor, window, cx| {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
snapshot.calculate_relative_line_numbers(
|
||
&(DisplayRow(0)..DisplayRow(6)),
|
||
DisplayRow(3),
|
||
false,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert_eq!(relative_rows[&DisplayRow(0)], 3);
|
||
assert_eq!(relative_rows[&DisplayRow(1)], 2);
|
||
assert_eq!(relative_rows[&DisplayRow(2)], 1);
|
||
// current line has no relative number
|
||
assert!(!relative_rows.contains_key(&DisplayRow(3)));
|
||
assert_eq!(relative_rows[&DisplayRow(4)], 1);
|
||
assert_eq!(relative_rows[&DisplayRow(5)], 2);
|
||
|
||
// works if cursor is before screen
|
||
let relative_rows = window
|
||
.update(cx, |editor, window, cx| {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
snapshot.calculate_relative_line_numbers(
|
||
&(DisplayRow(3)..DisplayRow(6)),
|
||
DisplayRow(1),
|
||
false,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert_eq!(relative_rows.len(), 3);
|
||
assert_eq!(relative_rows[&DisplayRow(3)], 2);
|
||
assert_eq!(relative_rows[&DisplayRow(4)], 3);
|
||
assert_eq!(relative_rows[&DisplayRow(5)], 4);
|
||
|
||
// works if cursor is after screen
|
||
let relative_rows = window
|
||
.update(cx, |editor, window, cx| {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
snapshot.calculate_relative_line_numbers(
|
||
&(DisplayRow(0)..DisplayRow(3)),
|
||
DisplayRow(6),
|
||
false,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert_eq!(relative_rows.len(), 3);
|
||
assert_eq!(relative_rows[&DisplayRow(0)], 5);
|
||
assert_eq!(relative_rows[&DisplayRow(1)], 4);
|
||
assert_eq!(relative_rows[&DisplayRow(2)], 3);
|
||
|
||
let gutter = Gutter {
|
||
row_infos: &(0..6)
|
||
.map(|row| RowInfo {
|
||
buffer_row: Some(row),
|
||
diff_status: (row == DELETED_LINE).then(|| {
|
||
DiffHunkStatus::deleted(
|
||
buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||
)
|
||
}),
|
||
..Default::default()
|
||
})
|
||
.collect::<Vec<_>>(),
|
||
..test_gutter(line_height, &snapshot)
|
||
};
|
||
|
||
const DELETED_LINE: u32 = 3;
|
||
let layouts = cx
|
||
.update_window(*window, |_, window, cx| {
|
||
element.layout_line_numbers(
|
||
&gutter,
|
||
&BTreeMap::default(),
|
||
Some(DisplayRow(0)),
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert_eq!(layouts.len(), 5,);
|
||
assert!(
|
||
layouts.get(&MultiBufferRow(DELETED_LINE)).is_none(),
|
||
"Deleted line should not have a line number"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_layout_line_numbers_with_folded_lines(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
|
||
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = cx.new(|cx| {
|
||
Buffer::local(
|
||
indoc::indoc! {"
|
||
fn test() -> int {
|
||
return 2;
|
||
}
|
||
|
||
fn another_test() -> int {
|
||
# This is a very peculiar method that is hard to grasp.
|
||
return 4;
|
||
}
|
||
"},
|
||
cx,
|
||
)
|
||
.with_language(python_lang, cx)
|
||
});
|
||
|
||
let buffer = MultiBuffer::build_from_buffer(buffer, cx);
|
||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||
});
|
||
|
||
let editor = window.root(cx).unwrap();
|
||
let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
|
||
let line_height = window
|
||
.update(cx, |_, window, _| {
|
||
style.text.line_height_in_pixels(window.rem_size())
|
||
})
|
||
.unwrap();
|
||
let element = EditorElement::new(&editor, style);
|
||
let snapshot = window
|
||
.update(cx, |editor, window, cx| {
|
||
editor.fold_at(MultiBufferRow(0), window, cx);
|
||
editor.snapshot(window, cx)
|
||
})
|
||
.unwrap();
|
||
|
||
let layouts = cx
|
||
.update_window(*window, |_, window, cx| {
|
||
element.layout_line_numbers(
|
||
&test_gutter(line_height, &snapshot),
|
||
&BTreeMap::default(),
|
||
Some(DisplayRow(3)),
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert_eq!(layouts.len(), 6);
|
||
|
||
let relative_rows = window
|
||
.update(cx, |editor, window, cx| {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
snapshot.calculate_relative_line_numbers(
|
||
&(DisplayRow(0)..DisplayRow(6)),
|
||
DisplayRow(3),
|
||
false,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert_eq!(relative_rows[&DisplayRow(0)], 3);
|
||
assert_eq!(relative_rows[&DisplayRow(1)], 2);
|
||
assert_eq!(relative_rows[&DisplayRow(2)], 1);
|
||
// current line has no relative number
|
||
assert!(!relative_rows.contains_key(&DisplayRow(3)));
|
||
assert_eq!(relative_rows[&DisplayRow(4)], 1);
|
||
assert_eq!(relative_rows[&DisplayRow(5)], 2);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_layout_line_numbers_wrapping(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
|
||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||
});
|
||
|
||
update_test_language_settings(cx, &|s| {
|
||
s.defaults.preferred_line_length = Some(5_u32);
|
||
s.defaults.soft_wrap = Some(language_settings::SoftWrap::Bounded);
|
||
});
|
||
|
||
let editor = window.root(cx).unwrap();
|
||
let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
|
||
let line_height = window
|
||
.update(cx, |_, window, _| {
|
||
style.text.line_height_in_pixels(window.rem_size())
|
||
})
|
||
.unwrap();
|
||
let element = EditorElement::new(&editor, style);
|
||
let snapshot = window
|
||
.update(cx, |editor, window, cx| editor.snapshot(window, cx))
|
||
.unwrap();
|
||
|
||
let layouts = cx
|
||
.update_window(*window, |_, window, cx| {
|
||
element.layout_line_numbers(
|
||
&test_gutter(line_height, &snapshot),
|
||
&BTreeMap::default(),
|
||
Some(DisplayRow(0)),
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert_eq!(layouts.len(), 3);
|
||
|
||
let relative_rows = window
|
||
.update(cx, |editor, window, cx| {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
snapshot.calculate_relative_line_numbers(
|
||
&(DisplayRow(0)..DisplayRow(6)),
|
||
DisplayRow(3),
|
||
true,
|
||
)
|
||
})
|
||
.unwrap();
|
||
|
||
assert_eq!(relative_rows[&DisplayRow(0)], 3);
|
||
assert_eq!(relative_rows[&DisplayRow(1)], 2);
|
||
assert_eq!(relative_rows[&DisplayRow(2)], 1);
|
||
// current line has no relative number
|
||
assert!(!relative_rows.contains_key(&DisplayRow(3)));
|
||
assert_eq!(relative_rows[&DisplayRow(4)], 1);
|
||
assert_eq!(relative_rows[&DisplayRow(5)], 2);
|
||
|
||
let layouts = cx
|
||
.update_window(*window, |_, window, cx| {
|
||
element.layout_line_numbers(
|
||
&Gutter {
|
||
row_infos: &(0..6)
|
||
.map(|row| RowInfo {
|
||
buffer_row: Some(row),
|
||
diff_status: Some(DiffHunkStatus::deleted(
|
||
buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||
)),
|
||
..Default::default()
|
||
})
|
||
.collect::<Vec<_>>(),
|
||
..test_gutter(line_height, &snapshot)
|
||
},
|
||
&BTreeMap::from_iter([(DisplayRow(0), LineHighlightSpec::default())]),
|
||
Some(DisplayRow(0)),
|
||
window,
|
||
cx,
|
||
)
|
||
})
|
||
.unwrap();
|
||
assert!(
|
||
layouts.is_empty(),
|
||
"Deleted lines should have no line number"
|
||
);
|
||
|
||
let relative_rows = window
|
||
.update(cx, |editor, window, cx| {
|
||
let snapshot = editor.snapshot(window, cx);
|
||
snapshot.calculate_relative_line_numbers(
|
||
&(DisplayRow(0)..DisplayRow(6)),
|
||
DisplayRow(3),
|
||
true,
|
||
)
|
||
})
|
||
.unwrap();
|
||
|
||
// Deleted lines should still have relative numbers
|
||
assert_eq!(relative_rows[&DisplayRow(0)], 3);
|
||
assert_eq!(relative_rows[&DisplayRow(1)], 2);
|
||
assert_eq!(relative_rows[&DisplayRow(2)], 1);
|
||
// current line, even if deleted, has no relative number
|
||
assert!(!relative_rows.contains_key(&DisplayRow(3)));
|
||
assert_eq!(relative_rows[&DisplayRow(4)], 1);
|
||
assert_eq!(relative_rows[&DisplayRow(5)], 2);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_vim_visual_selections(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
|
||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||
});
|
||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||
let editor = window.root(cx).unwrap();
|
||
let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
|
||
|
||
window
|
||
.update(cx, |editor, window, cx| {
|
||
editor.cursor_offset_on_selection = true;
|
||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||
s.select_ranges([
|
||
Point::new(0, 0)..Point::new(1, 0),
|
||
Point::new(3, 2)..Point::new(3, 3),
|
||
Point::new(5, 6)..Point::new(6, 0),
|
||
]);
|
||
});
|
||
})
|
||
.unwrap();
|
||
|
||
let (_, state) = cx.draw(
|
||
point(px(500.), px(500.)),
|
||
size(px(500.), px(500.)),
|
||
|_, _| EditorElement::new(&editor, style),
|
||
);
|
||
|
||
assert_eq!(state.selections.len(), 1);
|
||
let local_selections = &state.selections[0].1;
|
||
assert_eq!(local_selections.len(), 3);
|
||
// moves cursor back one line
|
||
assert_eq!(
|
||
local_selections[0].head,
|
||
DisplayPoint::new(DisplayRow(0), 6)
|
||
);
|
||
assert_eq!(
|
||
local_selections[0].range,
|
||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0)
|
||
);
|
||
|
||
// moves cursor back one column
|
||
assert_eq!(
|
||
local_selections[1].range,
|
||
DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 3)
|
||
);
|
||
assert_eq!(
|
||
local_selections[1].head,
|
||
DisplayPoint::new(DisplayRow(3), 2)
|
||
);
|
||
|
||
// leaves cursor on the max point
|
||
assert_eq!(
|
||
local_selections[2].range,
|
||
DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(6), 0)
|
||
);
|
||
assert_eq!(
|
||
local_selections[2].head,
|
||
DisplayPoint::new(DisplayRow(6), 0)
|
||
);
|
||
|
||
// active lines does not include 1 (even though the range of the selection does)
|
||
assert_eq!(
|
||
state.active_rows.keys().cloned().collect::<Vec<_>>(),
|
||
vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple("", cx);
|
||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||
});
|
||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||
let editor = window.root(cx).unwrap();
|
||
let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
|
||
window
|
||
.update(cx, |editor, window, cx| {
|
||
editor.set_placeholder_text("hello", window, cx);
|
||
editor.insert_blocks(
|
||
[BlockProperties {
|
||
style: BlockStyle::Fixed,
|
||
placement: BlockPlacement::Above(Anchor::Min),
|
||
height: Some(3),
|
||
render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
|
||
priority: 0,
|
||
}],
|
||
None,
|
||
cx,
|
||
);
|
||
|
||
// Blur the editor so that it displays placeholder text.
|
||
window.blur();
|
||
})
|
||
.unwrap();
|
||
|
||
let (_, state) = cx.draw(
|
||
point(px(500.), px(500.)),
|
||
size(px(500.), px(500.)),
|
||
|_, _| EditorElement::new(&editor, style),
|
||
);
|
||
assert_eq!(state.position_map.line_layouts.len(), 4);
|
||
assert_eq!(state.line_numbers.len(), 1);
|
||
assert_eq!(
|
||
state
|
||
.line_numbers
|
||
.get(&MultiBufferRow(0))
|
||
.map(|line_number| line_number
|
||
.segments
|
||
.first()
|
||
.unwrap()
|
||
.shaped_line
|
||
.text
|
||
.as_ref()),
|
||
Some("1")
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
|
||
const TAB_SIZE: u32 = 4;
|
||
|
||
let input_text = "\t \t|\t| a b";
|
||
let expected_invisibles = vec![
|
||
Invisible::Tab {
|
||
line_start_offset: 0,
|
||
line_end_offset: TAB_SIZE as usize,
|
||
},
|
||
Invisible::Whitespace {
|
||
line_start_offset: TAB_SIZE as usize,
|
||
line_end_offset: TAB_SIZE as usize + 1,
|
||
},
|
||
Invisible::Tab {
|
||
line_start_offset: TAB_SIZE as usize + 1,
|
||
line_end_offset: TAB_SIZE as usize * 2,
|
||
},
|
||
Invisible::Tab {
|
||
line_start_offset: TAB_SIZE as usize * 2 + 1,
|
||
line_end_offset: TAB_SIZE as usize * 3,
|
||
},
|
||
Invisible::Whitespace {
|
||
line_start_offset: TAB_SIZE as usize * 3 + 1,
|
||
line_end_offset: TAB_SIZE as usize * 3 + 2,
|
||
},
|
||
Invisible::Whitespace {
|
||
line_start_offset: TAB_SIZE as usize * 3 + 3,
|
||
line_end_offset: TAB_SIZE as usize * 3 + 4,
|
||
},
|
||
];
|
||
assert_eq!(
|
||
expected_invisibles.len(),
|
||
input_text
|
||
.chars()
|
||
.filter(|initial_char| initial_char.is_whitespace())
|
||
.count(),
|
||
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
|
||
);
|
||
|
||
for show_line_numbers in [true, false] {
|
||
init_test(cx, |s| {
|
||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||
s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
|
||
});
|
||
|
||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||
cx,
|
||
EditorMode::full(),
|
||
input_text,
|
||
px(500.0),
|
||
show_line_numbers,
|
||
);
|
||
|
||
assert_eq!(expected_invisibles, actual_invisibles);
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_multibyte_whitespace_uses_utf8_byte_offsets(cx: &mut TestAppContext) {
|
||
init_test(cx, |s| {
|
||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||
});
|
||
|
||
// Regression test for #49186. NBSP (U+00A0) is rendered via the invisible
|
||
// character `replacement` pipeline, which flushes the internal `line`
|
||
// scratch buffer mid-line. Any whitespace invisible that follows must use
|
||
// the absolute byte offset within the logical line (here: byte 4 for the
|
||
// trailing ASCII space), not an offset relative to the post-flush buffer.
|
||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||
cx,
|
||
EditorMode::full(),
|
||
"a\u{00A0}b ",
|
||
px(500.0),
|
||
false,
|
||
);
|
||
|
||
assert_eq!(
|
||
actual_invisibles,
|
||
vec![Invisible::Whitespace {
|
||
line_start_offset: 4,
|
||
line_end_offset: 5,
|
||
}]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_replacement_chunks_are_clipped_to_max_line_len(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple("", cx);
|
||
Editor::new(EditorMode::full(), buffer, None, window, cx)
|
||
});
|
||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||
let editor = window.root(cx).unwrap();
|
||
let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone()));
|
||
let editor_mode = EditorMode::full();
|
||
let max_line_len = "\u{00a0}abcdef".len();
|
||
|
||
window
|
||
.update(cx, |_, window, cx| {
|
||
let chunks = std::iter::once(HighlightedChunk {
|
||
text: "\u{00a0}",
|
||
style: None,
|
||
is_tab: false,
|
||
is_inlay: false,
|
||
replacement: Some(ChunkReplacement::Str("\u{2007}".into())),
|
||
})
|
||
.chain(std::iter::once(HighlightedChunk {
|
||
text: "abcdefghi",
|
||
style: None,
|
||
is_tab: false,
|
||
is_inlay: false,
|
||
replacement: None,
|
||
}))
|
||
.chain(
|
||
std::iter::repeat_with(|| HighlightedChunk {
|
||
text: "\u{00a0}",
|
||
style: None,
|
||
is_tab: false,
|
||
is_inlay: false,
|
||
replacement: Some(ChunkReplacement::Str("\u{2007}".into())),
|
||
})
|
||
.take(8),
|
||
);
|
||
|
||
let layouts = LineWithInvisibles::from_chunks(
|
||
chunks,
|
||
&style,
|
||
max_line_len,
|
||
1,
|
||
&editor_mode,
|
||
px(500.),
|
||
|_| false,
|
||
&[],
|
||
window,
|
||
cx,
|
||
);
|
||
|
||
assert_eq!(layouts.len(), 1);
|
||
assert_eq!(layouts[0].len, max_line_len);
|
||
assert!(layouts[0].fragments.len() <= max_line_len);
|
||
})
|
||
.unwrap();
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
|
||
init_test(cx, |s| {
|
||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||
s.defaults.tab_size = NonZeroU32::new(4);
|
||
});
|
||
|
||
for editor_mode_without_invisibles in [
|
||
EditorMode::SingleLine,
|
||
EditorMode::AutoHeight {
|
||
min_lines: 1,
|
||
max_lines: Some(100),
|
||
},
|
||
] {
|
||
for show_line_numbers in [true, false] {
|
||
let invisibles = collect_invisibles_from_new_editor(
|
||
cx,
|
||
editor_mode_without_invisibles.clone(),
|
||
"\t\t\t| | a b",
|
||
px(500.0),
|
||
show_line_numbers,
|
||
);
|
||
assert!(
|
||
invisibles.is_empty(),
|
||
"For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
|
||
let tab_size = 4;
|
||
let input_text = "a\tbcd ".repeat(9);
|
||
let repeated_invisibles = [
|
||
Invisible::Tab {
|
||
line_start_offset: 1,
|
||
line_end_offset: tab_size as usize,
|
||
},
|
||
Invisible::Whitespace {
|
||
line_start_offset: tab_size as usize + 3,
|
||
line_end_offset: tab_size as usize + 4,
|
||
},
|
||
Invisible::Whitespace {
|
||
line_start_offset: tab_size as usize + 4,
|
||
line_end_offset: tab_size as usize + 5,
|
||
},
|
||
Invisible::Whitespace {
|
||
line_start_offset: tab_size as usize + 5,
|
||
line_end_offset: tab_size as usize + 6,
|
||
},
|
||
Invisible::Whitespace {
|
||
line_start_offset: tab_size as usize + 6,
|
||
line_end_offset: tab_size as usize + 7,
|
||
},
|
||
Invisible::Whitespace {
|
||
line_start_offset: tab_size as usize + 7,
|
||
line_end_offset: tab_size as usize + 8,
|
||
},
|
||
];
|
||
let expected_invisibles = std::iter::once(repeated_invisibles)
|
||
.cycle()
|
||
.take(9)
|
||
.flatten()
|
||
.collect::<Vec<_>>();
|
||
assert_eq!(
|
||
expected_invisibles.len(),
|
||
input_text
|
||
.chars()
|
||
.filter(|initial_char| initial_char.is_whitespace())
|
||
.count(),
|
||
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
|
||
);
|
||
info!("Expected invisibles: {expected_invisibles:?}");
|
||
|
||
init_test(cx, |_| {});
|
||
|
||
// Put the same string with repeating whitespace pattern into editors of various size,
|
||
// take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
|
||
let resize_step = 10.0;
|
||
let mut editor_width = 200.0;
|
||
while editor_width <= 1000.0 {
|
||
for show_line_numbers in [true, false] {
|
||
update_test_language_settings(cx, &|s| {
|
||
s.defaults.tab_size = NonZeroU32::new(tab_size);
|
||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||
s.defaults.preferred_line_length = Some(editor_width as u32);
|
||
s.defaults.soft_wrap = Some(language_settings::SoftWrap::Bounded);
|
||
});
|
||
|
||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||
cx,
|
||
EditorMode::full(),
|
||
&input_text,
|
||
px(editor_width),
|
||
show_line_numbers,
|
||
);
|
||
|
||
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
|
||
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
|
||
let mut i = 0;
|
||
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
|
||
i = actual_index;
|
||
match expected_invisibles.get(i) {
|
||
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
|
||
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
|
||
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
|
||
_ => {
|
||
panic!(
|
||
"At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}"
|
||
)
|
||
}
|
||
},
|
||
None => {
|
||
panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
|
||
}
|
||
}
|
||
}
|
||
let missing_expected_invisibles = &expected_invisibles[i + 1..];
|
||
assert!(
|
||
missing_expected_invisibles.is_empty(),
|
||
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
|
||
);
|
||
|
||
editor_width += resize_step;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn collect_invisibles_from_new_editor(
|
||
cx: &mut TestAppContext,
|
||
editor_mode: EditorMode,
|
||
input_text: &str,
|
||
editor_width: Pixels,
|
||
show_line_numbers: bool,
|
||
) -> Vec<Invisible> {
|
||
info!(
|
||
"Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
|
||
f32::from(editor_width)
|
||
);
|
||
let window = cx.add_window(|window, cx| {
|
||
let buffer = MultiBuffer::build_simple(input_text, cx);
|
||
Editor::new(editor_mode, buffer, None, window, cx)
|
||
});
|
||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||
let editor = window.root(cx).unwrap();
|
||
|
||
let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
|
||
window
|
||
.update(cx, |editor, _, cx| {
|
||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||
editor.set_wrap_width(Some(editor_width), cx);
|
||
editor.set_show_line_numbers(show_line_numbers, cx);
|
||
})
|
||
.unwrap();
|
||
let (_, state) = cx.draw(
|
||
point(px(500.), px(500.)),
|
||
size(px(500.), px(500.)),
|
||
|_, _| EditorElement::new(&editor, style),
|
||
);
|
||
state
|
||
.position_map
|
||
.line_layouts
|
||
.iter()
|
||
.flat_map(|line_with_invisibles| &line_with_invisibles.invisibles)
|
||
.cloned()
|
||
.collect()
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_merge_overlapping_ranges() {
|
||
let base_bg = Hsla::white();
|
||
let color1 = Hsla {
|
||
h: 0.0,
|
||
s: 0.5,
|
||
l: 0.5,
|
||
a: 0.5,
|
||
};
|
||
let color2 = Hsla {
|
||
h: 120.0,
|
||
s: 0.5,
|
||
l: 0.5,
|
||
a: 0.5,
|
||
};
|
||
|
||
let display_point = |col| DisplayPoint::new(DisplayRow(0), col);
|
||
let cols = |v: &Vec<(Range<DisplayPoint>, Hsla)>| -> Vec<(u32, u32)> {
|
||
v.iter()
|
||
.map(|(r, _)| (r.start.column(), r.end.column()))
|
||
.collect()
|
||
};
|
||
|
||
// Test overlapping ranges blend colors
|
||
let overlapping = vec![
|
||
(display_point(5)..display_point(15), color1),
|
||
(display_point(10)..display_point(20), color2),
|
||
];
|
||
let result = EditorElement::merge_overlapping_ranges(overlapping, base_bg);
|
||
assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
|
||
|
||
// Test middle segment should have blended color
|
||
let blended = Hsla::blend(Hsla::blend(base_bg, color1), color2);
|
||
assert_eq!(result[1].1, blended);
|
||
|
||
// Test adjacent same-color ranges merge
|
||
let adjacent_same = vec![
|
||
(display_point(5)..display_point(10), color1),
|
||
(display_point(10)..display_point(15), color1),
|
||
];
|
||
let result = EditorElement::merge_overlapping_ranges(adjacent_same, base_bg);
|
||
assert_eq!(cols(&result), vec![(5, 15)]);
|
||
|
||
// Test contained range splits
|
||
let contained = vec![
|
||
(display_point(5)..display_point(20), color1),
|
||
(display_point(10)..display_point(15), color2),
|
||
];
|
||
let result = EditorElement::merge_overlapping_ranges(contained, base_bg);
|
||
assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]);
|
||
|
||
// Test multiple overlaps split at every boundary
|
||
let color3 = Hsla {
|
||
h: 240.0,
|
||
s: 0.5,
|
||
l: 0.5,
|
||
a: 0.5,
|
||
};
|
||
let complex = vec![
|
||
(display_point(5)..display_point(12), color1),
|
||
(display_point(8)..display_point(16), color2),
|
||
(display_point(10)..display_point(14), color3),
|
||
];
|
||
let result = EditorElement::merge_overlapping_ranges(complex, base_bg);
|
||
assert_eq!(
|
||
cols(&result),
|
||
vec![(5, 8), (8, 10), (10, 12), (12, 14), (14, 16)]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_bg_segments_per_row() {
|
||
let base_bg = Hsla::white();
|
||
|
||
// Case A: selection spans three display rows: row 1 [5, end), full row 2, row 3 [0, 7)
|
||
{
|
||
let selection_color = Hsla {
|
||
h: 200.0,
|
||
s: 0.5,
|
||
l: 0.5,
|
||
a: 0.5,
|
||
};
|
||
let player_color = PlayerColor {
|
||
cursor: selection_color,
|
||
background: selection_color,
|
||
selection: selection_color,
|
||
};
|
||
|
||
let spanning_selection = SelectionLayout {
|
||
head: DisplayPoint::new(DisplayRow(3), 7),
|
||
cursor_shape: CursorShape::Bar,
|
||
is_newest: true,
|
||
is_local: true,
|
||
range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 7),
|
||
active_rows: DisplayRow(1)..DisplayRow(4),
|
||
user_name: None,
|
||
};
|
||
|
||
let selections = vec![(player_color, vec![spanning_selection])];
|
||
let result = EditorElement::bg_segments_per_row(
|
||
DisplayRow(0)..DisplayRow(5),
|
||
&selections,
|
||
[].into_iter(),
|
||
base_bg,
|
||
);
|
||
|
||
assert_eq!(result.len(), 5);
|
||
assert!(result[0].is_empty());
|
||
assert_eq!(result[1].len(), 1);
|
||
assert_eq!(result[2].len(), 1);
|
||
assert_eq!(result[3].len(), 1);
|
||
assert!(result[4].is_empty());
|
||
|
||
assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
|
||
assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
|
||
assert_eq!(result[1][0].0.end.column(), u32::MAX);
|
||
assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
|
||
assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
|
||
assert_eq!(result[2][0].0.end.column(), u32::MAX);
|
||
assert_eq!(result[3][0].0.start, DisplayPoint::new(DisplayRow(3), 0));
|
||
assert_eq!(result[3][0].0.end, DisplayPoint::new(DisplayRow(3), 7));
|
||
}
|
||
|
||
// Case B: selection ends exactly at the start of row 3, excluding row 3
|
||
{
|
||
let selection_color = Hsla {
|
||
h: 120.0,
|
||
s: 0.5,
|
||
l: 0.5,
|
||
a: 0.5,
|
||
};
|
||
let player_color = PlayerColor {
|
||
cursor: selection_color,
|
||
background: selection_color,
|
||
selection: selection_color,
|
||
};
|
||
|
||
let selection = SelectionLayout {
|
||
head: DisplayPoint::new(DisplayRow(2), 0),
|
||
cursor_shape: CursorShape::Bar,
|
||
is_newest: true,
|
||
is_local: true,
|
||
range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 0),
|
||
active_rows: DisplayRow(1)..DisplayRow(3),
|
||
user_name: None,
|
||
};
|
||
|
||
let selections = vec![(player_color, vec![selection])];
|
||
let result = EditorElement::bg_segments_per_row(
|
||
DisplayRow(0)..DisplayRow(4),
|
||
&selections,
|
||
[].into_iter(),
|
||
base_bg,
|
||
);
|
||
|
||
assert_eq!(result.len(), 4);
|
||
assert!(result[0].is_empty());
|
||
assert_eq!(result[1].len(), 1);
|
||
assert_eq!(result[2].len(), 1);
|
||
assert!(result[3].is_empty());
|
||
|
||
assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5));
|
||
assert_eq!(result[1][0].0.end.row(), DisplayRow(1));
|
||
assert_eq!(result[1][0].0.end.column(), u32::MAX);
|
||
assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0));
|
||
assert_eq!(result[2][0].0.end.row(), DisplayRow(2));
|
||
assert_eq!(result[2][0].0.end.column(), u32::MAX);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
fn generate_test_run(len: usize, color: Hsla) -> TextRun {
|
||
TextRun {
|
||
len,
|
||
color,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_split_runs_by_bg_segments(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let dx = |start: u32, end: u32| {
|
||
DisplayPoint::new(DisplayRow(0), start)..DisplayPoint::new(DisplayRow(0), end)
|
||
};
|
||
|
||
let text_color = Hsla {
|
||
h: 210.0,
|
||
s: 0.1,
|
||
l: 0.4,
|
||
a: 1.0,
|
||
};
|
||
let bg_1 = Hsla {
|
||
h: 30.0,
|
||
s: 0.6,
|
||
l: 0.8,
|
||
a: 1.0,
|
||
};
|
||
let bg_2 = Hsla {
|
||
h: 200.0,
|
||
s: 0.6,
|
||
l: 0.2,
|
||
a: 1.0,
|
||
};
|
||
let min_contrast = 45.0;
|
||
let adjusted_bg1 = ensure_minimum_contrast(text_color, bg_1, min_contrast);
|
||
let adjusted_bg2 = ensure_minimum_contrast(text_color, bg_2, min_contrast);
|
||
|
||
// Case A: single run; disjoint segments inside the run
|
||
{
|
||
let runs = vec![generate_test_run(20, text_color)];
|
||
let segs = vec![(dx(5, 10), bg_1), (dx(12, 16), bg_2)];
|
||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
|
||
// Expected slices: [0,5) [5,10) [10,12) [12,16) [16,20)
|
||
assert_eq!(
|
||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||
vec![5, 5, 2, 4, 4]
|
||
);
|
||
assert_eq!(out[0].color, text_color);
|
||
assert_eq!(out[1].color, adjusted_bg1);
|
||
assert_eq!(out[2].color, text_color);
|
||
assert_eq!(out[3].color, adjusted_bg2);
|
||
assert_eq!(out[4].color, text_color);
|
||
}
|
||
|
||
// Case B: multiple runs; segment extends to end of line (u32::MAX)
|
||
{
|
||
let runs = vec![
|
||
generate_test_run(8, text_color),
|
||
generate_test_run(7, text_color),
|
||
];
|
||
let segs = vec![(dx(6, u32::MAX), bg_1)];
|
||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
|
||
// Expected slices across runs: [0,6) [6,8) | [0,7)
|
||
assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![6, 2, 7]);
|
||
assert_eq!(out[0].color, text_color);
|
||
assert_eq!(out[1].color, adjusted_bg1);
|
||
assert_eq!(out[2].color, adjusted_bg1);
|
||
}
|
||
|
||
// Case C: multi-byte characters
|
||
{
|
||
// for text: "Hello 🌍 世界!"
|
||
let runs = vec![
|
||
generate_test_run(5, text_color), // "Hello"
|
||
generate_test_run(6, text_color), // " 🌍 "
|
||
generate_test_run(6, text_color), // "世界"
|
||
generate_test_run(1, text_color), // "!"
|
||
];
|
||
// selecting "🌍 世"
|
||
let segs = vec![(dx(6, 14), bg_1)];
|
||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
|
||
// "Hello" | " " | "🌍 " | "世" | "界" | "!"
|
||
assert_eq!(
|
||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||
vec![5, 1, 5, 3, 3, 1]
|
||
);
|
||
assert_eq!(out[0].color, text_color); // "Hello"
|
||
assert_eq!(out[2].color, adjusted_bg1); // "🌍 "
|
||
assert_eq!(out[3].color, adjusted_bg1); // "世"
|
||
assert_eq!(out[4].color, text_color); // "界"
|
||
assert_eq!(out[5].color, text_color); // "!"
|
||
}
|
||
|
||
// Case D: split multiple consecutive text runs with segments
|
||
{
|
||
let segs = vec![
|
||
(dx(2, 4), bg_1), // selecting "cd"
|
||
(dx(4, 8), bg_2), // selecting "efgh"
|
||
(dx(9, 11), bg_1), // selecting "jk"
|
||
(dx(12, 16), bg_2), // selecting "mnop"
|
||
(dx(18, 19), bg_1), // selecting "s"
|
||
];
|
||
|
||
// for text: "abcdef"
|
||
let runs = vec![
|
||
generate_test_run(2, text_color), // ab
|
||
generate_test_run(4, text_color), // cdef
|
||
];
|
||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 0);
|
||
// new splits "ab", "cd", "ef"
|
||
assert_eq!(out.iter().map(|r| r.len).collect::<Vec<_>>(), vec![2, 2, 2]);
|
||
assert_eq!(out[0].color, text_color);
|
||
assert_eq!(out[1].color, adjusted_bg1);
|
||
assert_eq!(out[2].color, adjusted_bg2);
|
||
|
||
// for text: "ghijklmn"
|
||
let runs = vec![
|
||
generate_test_run(3, text_color), // ghi
|
||
generate_test_run(2, text_color), // jk
|
||
generate_test_run(3, text_color), // lmn
|
||
];
|
||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 6); // 2 + 4 from first run
|
||
// new splits "gh", "i", "jk", "l", "mn"
|
||
assert_eq!(
|
||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||
vec![2, 1, 2, 1, 2]
|
||
);
|
||
assert_eq!(out[0].color, adjusted_bg2);
|
||
assert_eq!(out[1].color, text_color);
|
||
assert_eq!(out[2].color, adjusted_bg1);
|
||
assert_eq!(out[3].color, text_color);
|
||
assert_eq!(out[4].color, adjusted_bg2);
|
||
|
||
// for text: "opqrs"
|
||
let runs = vec![
|
||
generate_test_run(1, text_color), // o
|
||
generate_test_run(4, text_color), // pqrs
|
||
];
|
||
let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast, 14); // 6 + 3 + 2 + 3 from first two runs
|
||
// new splits "o", "p", "qr", "s"
|
||
assert_eq!(
|
||
out.iter().map(|r| r.len).collect::<Vec<_>>(),
|
||
vec![1, 1, 2, 1]
|
||
);
|
||
assert_eq!(out[0].color, adjusted_bg2);
|
||
assert_eq!(out[1].color, adjusted_bg2);
|
||
assert_eq!(out[2].color, text_color);
|
||
assert_eq!(out[3].color, adjusted_bg1);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_spacer_pattern_period() {
|
||
// line height is smaller than target height, so we just return half the line height
|
||
assert_eq!(EditorElement::spacer_pattern_period(10.0, 20.0), 5.0);
|
||
|
||
// line height is exactly half the target height, perfect match
|
||
assert_eq!(EditorElement::spacer_pattern_period(20.0, 10.0), 10.0);
|
||
|
||
// line height is close to half the target height
|
||
assert_eq!(EditorElement::spacer_pattern_period(20.0, 9.0), 10.0);
|
||
|
||
// line height is close to 1/4 the target height
|
||
assert_eq!(EditorElement::spacer_pattern_period(20.0, 4.8), 5.0);
|
||
}
|
||
|
||
#[gpui::test(iterations = 100)]
|
||
fn test_random_spacer_pattern_period(mut rng: StdRng) {
|
||
let line_height = rng.next_u32() as f32;
|
||
let target_height = rng.next_u32() as f32;
|
||
|
||
let result = EditorElement::spacer_pattern_period(line_height, target_height);
|
||
|
||
let k = line_height / result;
|
||
assert!(k - k.round() < 0.0000001); // approximately integer
|
||
assert!((k.round() as u32).is_multiple_of(2));
|
||
}
|
||
|
||
#[test]
|
||
fn test_calculate_wrap_width() {
|
||
let editor_width = px(800.0);
|
||
let em_width = px(8.0);
|
||
|
||
assert_eq!(
|
||
calculate_wrap_width(SoftWrap::GitDiff, editor_width, em_width),
|
||
None,
|
||
);
|
||
|
||
assert_eq!(
|
||
calculate_wrap_width(SoftWrap::None, editor_width, em_width),
|
||
Some(px((MAX_LINE_LEN as f32 / 2.0 * 8.0).ceil())),
|
||
);
|
||
|
||
assert_eq!(
|
||
calculate_wrap_width(SoftWrap::EditorWidth, editor_width, em_width),
|
||
Some(px(800.0)),
|
||
);
|
||
|
||
assert_eq!(
|
||
calculate_wrap_width(SoftWrap::Bounded(72), editor_width, em_width),
|
||
Some(px((72.0 * 8.0_f32).ceil())),
|
||
);
|
||
assert_eq!(
|
||
calculate_wrap_width(SoftWrap::Bounded(200), px(400.0), em_width),
|
||
Some(px(400.0)),
|
||
);
|
||
}
|
||
}
|