use std::ops::Range; use std::rc::Rc; use gpui::{ AnyElement, App, Bounds, Corners, Edges, Element, ElementId, ElementInputHandler, Entity, GlobalElementId, Half, HighlightStyle, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent, Path, Pixels, Point, Position, ShapedLine, SharedString, Size, Style, Styled as _, TextAlign, TextRun, TextStyle, UnderlineStyle, Window, fill, point, px, relative, size, }; use ropey::Rope; use smallvec::SmallVec; use theme::ActiveTheme; use super::mode::InputMode; use super::{InputState, LastLayout, WhitespaceIndicators}; use crate::button::{Button, ButtonVariants as _}; use crate::input::RopeExt as _; use crate::input::blink_cursor::CURSOR_WIDTH; use crate::input::display_map::LineLayout; use crate::scroll::Scrollbar; use crate::{IconName, Root, Selectable, Sizable as _}; const BOTTOM_MARGIN_ROWS: usize = 3; pub(super) const RIGHT_MARGIN: Pixels = px(10.); pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.); const FOLD_ICON_WIDTH: Pixels = px(14.); const FOLD_ICON_HITBOX_WIDTH: Pixels = px(18.); const MAX_HIGHLIGHT_LINE_LENGTH: usize = 10_000; #[derive(Clone, Copy, Debug, PartialEq)] struct EditorScrollbarLayout { bounds: Bounds, scroll_size: Size, } #[derive(Clone, Copy, Debug, PartialEq)] pub(super) struct EditorScrollbarSnapshot { layout: EditorScrollbarLayout, cursor_scroll_offset: Point, soft_wrap: bool, } impl EditorScrollbarSnapshot { fn new( input_bounds: Bounds, last_layout: &LastLayout, scroll_size: Size, cursor_scroll_offset: Point, state: &InputState, ) -> Self { Self { layout: EditorScrollbarLayout::new( input_bounds, last_layout.line_number_width, scroll_size, state.editor_scrollbar_paddings.get(), ), cursor_scroll_offset, soft_wrap: state.soft_wrap, } } } impl EditorScrollbarLayout { fn new( input_bounds: Bounds, line_number_width: Pixels, scroll_size: Size, paddings: Edges, ) -> Self { let left = if line_number_width == px(0.) { px(0.) } else { paddings.left + line_number_width - LINE_NUMBER_RIGHT_MARGIN }; Self { bounds: Bounds::new( point( input_bounds.origin.x + left, input_bounds.origin.y - paddings.top, ), size( input_bounds.size.width - left + paddings.right, input_bounds.size.height + paddings.top + paddings.bottom, ), ), scroll_size: size( scroll_size.width - left + paddings.right + RIGHT_MARGIN, scroll_size.height, ), } } } pub(super) struct EditorScrollbar { state: Entity, } impl EditorScrollbar { pub(super) fn new(state: Entity) -> Self { Self { state } } } impl IntoElement for EditorScrollbar { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for EditorScrollbar { type PrepaintState = Option; type RequestLayoutState = (); fn id(&self) -> Option { Some("editor-scrollbar".into()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.position = Position::Absolute; style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); (window.request_layout(style, [], cx), ()) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, _: Bounds, _: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { let state = self.state.read(cx); let snapshot = state.editor_scrollbar_snapshot.get()?; let scroll_handle = state.scroll_handle.clone(); if scroll_handle.offset() != snapshot.cursor_scroll_offset { scroll_handle.set_offset(snapshot.cursor_scroll_offset); } let mut scrollbar = if !snapshot.soft_wrap { Scrollbar::new(&scroll_handle) } else { Scrollbar::vertical(&scroll_handle) } .scroll_size(snapshot.layout.scroll_size) .into_any_element(); scrollbar.prepaint_as_root( snapshot.layout.bounds.origin, snapshot.layout.bounds.size.into(), window, cx, ); Some(scrollbar) } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, _: Bounds, _: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { if let Some(scrollbar) = prepaint.as_mut() { scrollbar.paint(window, cx); } } } fn clamp_auto_grow_vertical_scroll_offset( mode: &InputMode, scroll_top: Pixels, scroll_height: Pixels, input_height: Pixels, ) -> Pixels { if mode.is_auto_grow() { scroll_top.clamp((input_height - scroll_height).min(px(0.)), px(0.)) } else { scroll_top } } use super::MASK_CHAR; /// Convert a byte offset in the original text to a byte offset in the masked display string. /// /// The masked string consists of `MASK_CHAR` repeated once per character in the original text. /// Since `MASK_CHAR` may be multi-byte in UTF-8, the byte offset in the masked string is /// `char_index * MASK_CHAR.len_utf8()`. fn masked_display_offset(text: &Rope, original_offset: usize) -> usize { text.offset_to_char_index(original_offset) * MASK_CHAR.len_utf8() } /// Layout information for fold icons. struct FoldIconLayout { /// Hitbox for the line number area (used for hover detection) line_number_hitbox: Hitbox, /// List of (display_row, is_folded, icon_element) pairs for each fold candidate icons: Vec<(usize, bool, gpui::AnyElement)>, } pub(super) struct TextElement { pub(crate) state: Entity, placeholder: SharedString, } impl TextElement { pub(super) fn new(state: Entity) -> Self { Self { state, placeholder: SharedString::default(), } } /// Set the placeholder text of the input field. pub fn placeholder(mut self, placeholder: impl Into) -> Self { self.placeholder = placeholder.into(); self } fn paint_mouse_listeners(&mut self, window: &mut Window, _: &mut App) { window.on_mouse_event({ let state = self.state.clone(); move |event: &MouseMoveEvent, _, window, cx| { if event.pressed_button == Some(MouseButton::Left) { state.update(cx, |state, cx| { state.on_drag_move(event, window, cx); }); } } }); window.on_mouse_event({ let state = self.state.clone(); move |_: &MouseUpEvent, phase, _, cx| { if !phase.bubble() { return; } // Stop auto-scroll when mouse up, and also stop selecting. state.update(cx, |state, _| { state.selecting = false; }); } }); } /// Returns the: /// /// - cursor bounds /// - scroll offset /// - current row index (No only the visible lines, but all lines) /// /// This method also will update for track scroll to cursor. fn layout_cursor( &self, last_layout: &LastLayout, bounds: &mut Bounds, scroll_size: Size, _: &mut Window, cx: &mut App, ) -> (Option>, Point, Option) { let state = self.state.read(cx); let line_height = last_layout.line_height; let visible_range = &last_layout.visible_range; let lines = &last_layout.lines; let line_number_width = last_layout.line_number_width; let mut selected_range = state.selected_range; if let Some(ime_marked_range) = &state.ime_marked_range { selected_range = (ime_marked_range.end..ime_marked_range.end).into(); } let is_selected_all = selected_range.len() == state.text.len(); let mut cursor = state.cursor(); if state.masked { selected_range.start = masked_display_offset(&state.text, selected_range.start); selected_range.end = masked_display_offset(&state.text, selected_range.end); cursor = masked_display_offset(&state.text, cursor); } let mut current_row = None; let mut scroll_offset = state.scroll_handle.offset(); let mut cursor_bounds = None; // If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input. let top_bottom_margin = if state.mode.is_auto_grow() { line_height } else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 { line_height } else { BOTTOM_MARGIN_ROWS * line_height }; // The cursor corresponds to the current cursor position in the text no only the line. let mut cursor_pos = None; let mut cursor_start = None; let mut cursor_end = None; let mut prev_lines_offset = 0; let mut offset_y = px(0.); let buffer_lines = state.display_map.lines(); let visible_buffer_lines = &last_layout.visible_buffer_lines; let mut vi = 0; // index into visible_buffer_lines / lines for (ix, wrap_line) in buffer_lines.iter().enumerate() { let row = ix; let line_origin = point(px(0.), offset_y); // break loop if all cursor positions are found if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() { break; } // Check if this buffer line has a LineLayout in the compact lines vec let line_layout = if vi < visible_buffer_lines.len() && visible_buffer_lines[vi] == ix { let l = &lines[vi]; vi += 1; Some(l) } else { None }; if let Some(line) = line_layout { if cursor_pos.is_none() { let offset = cursor.saturating_sub(prev_lines_offset); if let Some(pos) = line.position_for_index(offset, last_layout, state.cursor_line_end_affinity) { current_row = Some(row); cursor_pos = Some(line_origin + pos); } } if cursor_start.is_none() { let offset = selected_range.start.saturating_sub(prev_lines_offset); if let Some(pos) = line.position_for_index(offset, last_layout, false) { cursor_start = Some(line_origin + pos); } } if cursor_end.is_none() { let offset = selected_range.end.saturating_sub(prev_lines_offset); if let Some(pos) = line.position_for_index(offset, last_layout, false) { cursor_end = Some(line_origin + pos); } } offset_y += line.size(line_height).height; // +1 for the last `\n` prev_lines_offset += wrap_line.len() + 1; } else { // Not visible (before visible range or hidden/folded). // Just increase the offset_y and prev_lines_offset for scroll tracking. if prev_lines_offset >= cursor && cursor_pos.is_none() { current_row = Some(row); cursor_pos = Some(line_origin); } if prev_lines_offset >= selected_range.start && cursor_start.is_none() { cursor_start = Some(line_origin); } if prev_lines_offset >= selected_range.end && cursor_end.is_none() { cursor_end = Some(line_origin); } let visible_wrap_rows = state.display_map.visible_wrap_row_count_for_buffer_line(ix); offset_y += line_height * visible_wrap_rows; // +1 for the last `\n` prev_lines_offset += wrap_line.len() + 1; } } if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) = (cursor_pos, cursor_start, cursor_end) { let selection_changed = state.last_selected_range != Some(selected_range); if selection_changed && !is_selected_all { // For Right alignment use 0 margin: cursor is clamped to bounds separately, // so we never scroll the text for cursor-at-edge, avoiding a first-click jump. let safety_margin = match last_layout.text_align { TextAlign::Left => RIGHT_MARGIN, TextAlign::Right => px(0.), TextAlign::Center => CURSOR_WIDTH, }; scroll_offset.x = if scroll_offset.x + cursor_pos.x > (bounds.size.width - line_number_width - safety_margin) { // cursor is out of right bounds.size.width - line_number_width - safety_margin - cursor_pos.x } else if scroll_offset.x + cursor_pos.x < px(0.) { // cursor is out of left scroll_offset.x - cursor_pos.x } else { scroll_offset.x }; // If we change the scroll_offset.y, GPUI will render and trigger the next run loop. // So, here we just adjust offset by `line_height` for move smooth. scroll_offset.y = if scroll_offset.y + cursor_pos.y > bounds.size.height - top_bottom_margin { // cursor is out of bottom scroll_offset.y - line_height } else if scroll_offset.y + cursor_pos.y < top_bottom_margin { // cursor is out of top (scroll_offset.y + line_height).min(px(0.)) } else { scroll_offset.y }; // For selection to move scroll if state.selection_reversed { if scroll_offset.x + cursor_start.x < px(0.) { // selection start is out of left scroll_offset.x = -cursor_start.x; } if scroll_offset.y + cursor_start.y < px(0.) { // selection start is out of top scroll_offset.y = -cursor_start.y; } } else { // TODO: Consider to remove this part, // maybe is not necessary (But selection_reversed is needed). if scroll_offset.x + cursor_end.x <= px(0.) { // selection end is out of left scroll_offset.x = -cursor_end.x; } if scroll_offset.y + cursor_end.y <= px(0.) { // selection end is out of top scroll_offset.y = -cursor_end.y; } } } // cursor bounds let cursor_height = match state.size { crate::Size::Large => 1., crate::Size::Small => 0.75, _ => 0.85, } * line_height; // For Right alignment, clamp cursor within the right edge of bounds so it // stays visible without having to shift the text via scroll_offset. let cursor_x = bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x; let cursor_x = if last_layout.text_align == TextAlign::Right { cursor_x.min(bounds.right() - CURSOR_WIDTH) } else { cursor_x }; cursor_bounds = Some(Bounds::new( point( cursor_x, bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.), ), size(CURSOR_WIDTH, cursor_height), )); } if let Some(deferred_scroll_offset) = state.deferred_scroll_offset { scroll_offset = deferred_scroll_offset; } scroll_offset.y = clamp_auto_grow_vertical_scroll_offset( &state.mode, scroll_offset.y, scroll_size.height, bounds.size.height, ); bounds.origin += scroll_offset; (cursor_bounds, scroll_offset, current_row) } /// Layout the match range to a Path. pub(crate) fn layout_match_range( range: Range, last_layout: &LastLayout, bounds: &Bounds, ) -> Option> { if range.is_empty() { return None; } if range.start < last_layout.visible_range_offset.start || range.end > last_layout.visible_range_offset.end { return None; } let line_height = last_layout.line_height; let visible_top = last_layout.visible_top; let lines = &last_layout.lines; let line_number_width = last_layout.line_number_width; let start_ix = range.start; let end_ix = range.end; // Start from visible_top (which already accounts for all lines before visible range) let mut offset_y = visible_top; let mut line_corners = vec![]; // Iterate only over visible (non-hidden) buffer lines for (prev_lines_offset, line) in last_layout .visible_line_byte_offsets .iter() .zip(lines.iter()) { let prev_lines_offset = *prev_lines_offset; let line_size = line.size(line_height); let line_wrap_width = line_size.width; let line_origin = point(px(0.), offset_y); let line_cursor_start = line.position_for_index( start_ix.saturating_sub(prev_lines_offset), last_layout, false, ); let line_cursor_end = line.position_for_index( end_ix.saturating_sub(prev_lines_offset), last_layout, false, ); if line_cursor_start.is_some() || line_cursor_end.is_some() { let start = line_cursor_start .unwrap_or_else(|| line.position_for_index(0, last_layout, false).unwrap()); let end = line_cursor_end.unwrap_or_else(|| { line.position_for_index(line.len(), last_layout, false) .unwrap() }); // Split the selection into multiple items let wrapped_lines = (end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize; let mut end_x = end.x; if wrapped_lines > 0 { end_x = line_wrap_width; } // Ensure at least 6px width for the selection for empty lines. end_x = end_x.max(start.x + px(6.)); line_corners.push(Corners { top_left: line_origin + point(start.x, start.y), top_right: line_origin + point(end_x, start.y), bottom_left: line_origin + point(start.x, start.y + line_height), bottom_right: line_origin + point(end_x, start.y + line_height), }); // wrapped lines for i in 1..=wrapped_lines { let start = point(px(0.), start.y + i as f32 * line_height); let mut end = point(end.x, end.y + i as f32 * line_height); if i < wrapped_lines { end.x = line_size.width; } line_corners.push(Corners { top_left: line_origin + point(start.x, start.y), top_right: line_origin + point(end.x, start.y), bottom_left: line_origin + point(start.x, start.y + line_height), bottom_right: line_origin + point(end.x, start.y + line_height), }); } } if line_cursor_start.is_some() && line_cursor_end.is_some() { break; } offset_y += line_size.height; } let mut points = vec![]; if line_corners.is_empty() { return None; } // Fix corners to make sure the left to right direction for corners in &mut line_corners { if corners.top_left.x > corners.top_right.x { std::mem::swap(&mut corners.top_left, &mut corners.top_right); std::mem::swap(&mut corners.bottom_left, &mut corners.bottom_right); } } for corners in &line_corners { points.push(corners.top_right); points.push(corners.bottom_right); points.push(corners.bottom_left); } let mut rev_line_corners = line_corners.iter().rev().peekable(); while let Some(corners) = rev_line_corners.next() { points.push(corners.top_left); if let Some(next) = rev_line_corners.peek() && next.top_left.x > corners.top_left.x { points.push(point(next.top_left.x, corners.top_left.y)); } } // print_points_as_svg_path(&line_corners, &points); let path_origin = bounds.origin + point(line_number_width, px(0.)); let first_p = *points.first().unwrap(); let mut builder = gpui::PathBuilder::fill(); builder.move_to(path_origin + first_p); for p in points.iter().skip(1) { builder.line_to(path_origin + *p); } builder.build().ok() } fn layout_search_matches( &self, _last_layout: &LastLayout, _bounds: &Bounds, _cx: &mut App, ) -> Vec<(Path, bool)> { vec![] } fn layout_hover_highlight( &self, _last_layout: &LastLayout, _bounds: &Bounds, _cx: &mut App, ) -> Option> { None } fn layout_document_colors( &self, document_colors: &[(Range, Hsla)], last_layout: &LastLayout, bounds: &Bounds, _cx: &mut App, ) -> Vec<(Path, Hsla)> { let mut paths = vec![]; for (range, color) in document_colors.iter() { if let Some(path) = Self::layout_match_range(range.clone(), last_layout, bounds) { paths.push((path, *color)); } } paths } fn layout_selections( &self, last_layout: &LastLayout, bounds: &mut Bounds, window: &mut Window, cx: &mut App, ) -> Option> { let state = self.state.read(cx); if !state.focus_handle.is_focused(window) { return None; } let mut selected_range = state.selected_range; if let Some(ime_marked_range) = &state.ime_marked_range && !ime_marked_range.is_empty() { selected_range = (ime_marked_range.end..ime_marked_range.end).into(); } if selected_range.is_empty() { return None; } if state.masked { selected_range.start = masked_display_offset(&state.text, selected_range.start); selected_range.end = masked_display_offset(&state.text, selected_range.end); } let (start_ix, end_ix) = if selected_range.start < selected_range.end { (selected_range.start, selected_range.end) } else { (selected_range.end, selected_range.start) }; let range = start_ix.max(last_layout.visible_range_offset.start) ..end_ix.min(last_layout.visible_range_offset.end); Self::layout_match_range(range, last_layout, bounds) } /// Calculate the visible range of lines in the viewport. /// /// Returns /// /// - visible_range: The visible range is based on unwrapped lines (Zero based). /// - visible_buffer_lines: Indices of non-hidden buffer lines within the visible range. /// - visible_top: The top position of the first visible line in the scroll viewport. fn calculate_visible_range( &self, state: &InputState, line_height: Pixels, input_height: Pixels, ) -> (Range, Vec, Pixels) { // Add extra rows to avoid showing empty space when scroll to bottom. let extra_rows = 1; let mut visible_top = px(0.); if state.mode.is_single_line() { return (0..1, vec![0], visible_top); } let total_lines = state.display_map.wrap_row_count(); let mut scroll_top = if let Some(deferred_scroll_offset) = state.deferred_scroll_offset { deferred_scroll_offset.y } else { state.scroll_handle.offset().y }; let mut visible_range = 0..total_lines; scroll_top = clamp_auto_grow_vertical_scroll_offset( &state.mode, scroll_top, line_height * total_lines, input_height, ); let mut line_bottom = px(0.); for (ix, _line) in state.display_map.lines().iter().enumerate() { let visible_wrap_rows = state.display_map.visible_wrap_row_count_for_buffer_line(ix); if visible_wrap_rows == 0 { continue; } let wrapped_height = line_height * visible_wrap_rows; line_bottom += wrapped_height; if line_bottom < -scroll_top { visible_top = line_bottom - wrapped_height; visible_range.start = ix; } if line_bottom + scroll_top >= input_height { visible_range.end = (ix + extra_rows).min(total_lines); break; } } // Collect non-hidden buffer lines within the visible range let mut visible_buffer_lines = Vec::with_capacity(visible_range.len()); for ix in visible_range.start..visible_range.end { let visible_wrap_rows = state.display_map.visible_wrap_row_count_for_buffer_line(ix); if visible_wrap_rows > 0 { visible_buffer_lines.push(ix); } } (visible_range, visible_buffer_lines, visible_top) } /// Return (line_number_width, line_number_len) fn layout_line_numbers( state: &InputState, text: &Rope, font_size: Pixels, style: &TextStyle, window: &mut Window, ) -> (Pixels, usize) { let total_lines = text.lines_len(); let line_number_len = match total_lines { 0..=9999 => 5, 10000..=99999 => 6, 100000..=999999 => 7, _ => 8, }; let mut line_number_width = if state.mode.line_number() { let empty_line_number = window.text_system().shape_line( "+".repeat(line_number_len).into(), font_size, &[TextRun { len: line_number_len, font: style.font(), color: gpui::black(), background_color: None, underline: None, strikethrough: None, }], None, ); empty_line_number.width + LINE_NUMBER_RIGHT_MARGIN } else if state.mode.is_code_editor() && state.mode.is_multi_line() { LINE_NUMBER_RIGHT_MARGIN } else { px(0.) }; if state.mode.is_folding() { // Add extra space for fold icons line_number_width += FOLD_ICON_HITBOX_WIDTH } (line_number_width, line_number_len) } /// Layout shaped lines for whitespace indicators (space and tab). /// /// Returns `WhitespaceIndicators` with shaped lines for space and tab characters. fn layout_whitespace_indicators( state: &InputState, text_size: Pixels, style: &TextStyle, window: &mut Window, cx: &App, ) -> Option { if !state.show_whitespaces { return None; } let invisible_color = cx.theme().text_muted; let space_font_size = text_size.half(); let tab_font_size = text_size; let space_text = SharedString::new_static("•"); let space = window.text_system().shape_line( space_text.clone(), space_font_size, &[TextRun { len: space_text.len(), font: style.font(), color: invisible_color, background_color: None, underline: None, strikethrough: None, }], None, ); let tab_text = SharedString::new_static("→"); let tab = window.text_system().shape_line( tab_text.clone(), tab_font_size, &[TextRun { len: tab_text.len(), font: style.font(), color: invisible_color, background_color: None, underline: None, strikethrough: None, }], None, ); Some(WhitespaceIndicators { space, tab }) } /// Compute inline completion ghost lines for rendering. /// /// Returns (first_line, ghost_lines) where: /// - first_line: Shaped text for the first line (goes after cursor on same line) /// - ghost_lines: Shaped lines for subsequent lines (shift content down) fn layout_inline_completion( _state: &InputState, _visible_range: &Range, _font_size: Pixels, _window: &mut Window, _cx: &App, ) -> (Option, Vec) { (None, vec![]) } /// Return (line_number_width, line_number_len) /// Layout fold icon hitboxes during prepaint phase. /// /// This creates hitboxes for the fold icon area, positioned to the right of line numbers. /// Icons are created and prepainted here to avoid panics. fn layout_fold_icons( &self, origin_x: Pixels, bounds: &Bounds, last_layout: &LastLayout, window: &mut Window, cx: &mut App, ) -> FoldIconLayout { // First pass: collect fold information from state struct FoldInfo { buffer_line: usize, is_folded: bool, display_row: usize, offset_y: Pixels, } let line_number_hitbox = window.insert_hitbox( Bounds::new( point(origin_x, bounds.origin.y + last_layout.visible_top), size(last_layout.line_number_width, bounds.size.height), ), HitboxBehavior::Normal, ); let mut icon_layout = FoldIconLayout { line_number_hitbox, icons: vec![], }; let fold_infos: Vec = { let state = self.state.read(cx); if !state.mode.is_folding() { return icon_layout; } let mut infos = Vec::with_capacity(last_layout.visible_buffer_lines.len()); let mut offset_y = last_layout.visible_top; for (line, &buffer_line) in last_layout .lines .iter() .zip(last_layout.visible_buffer_lines.iter()) { if state.display_map.is_fold_candidate(buffer_line) { let is_folded = state.display_map.is_folded_at(buffer_line); infos.push(FoldInfo { buffer_line, is_folded, display_row: buffer_line, offset_y, }); } offset_y += line.wrapped_lines.len() * last_layout.line_height; } infos }; // state is dropped here // Second pass: create and prepaint icons let line_height = last_layout.line_height; let line_number_width = last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN - FOLD_ICON_HITBOX_WIDTH; let icon_relative_pos = point( (FOLD_ICON_HITBOX_WIDTH - FOLD_ICON_WIDTH).half(), (line_height - FOLD_ICON_WIDTH).half(), ); for (ix, info) in fold_infos.iter().enumerate() { // Position fold icon to the right of line numbers. // Use origin_x (unscrolled) so icons stay fixed in the gutter during horizontal scroll. let fold_icon_bounds = Bounds::new( point( origin_x + icon_relative_pos.x + line_number_width, bounds.origin.y + icon_relative_pos.y + info.offset_y, ), size(FOLD_ICON_HITBOX_WIDTH, line_height), ); // Create and prepaint icon let mut icon = Button::new(("fold", ix)) .ghost() .icon(if info.is_folded { IconName::CaretRight } else { IconName::CaretDown }) .xsmall() .rounded_xs() .size(FOLD_ICON_WIDTH) .selected(info.is_folded) .on_mouse_down(MouseButton::Left, { let state = self.state.clone(); let buffer_line = info.buffer_line; move |_, _: &mut Window, cx: &mut App| { cx.stop_propagation(); state.update(cx, |state, cx| { state.display_map.toggle_fold(buffer_line); cx.notify(); }); } }) .into_any_element(); icon.prepaint_as_root( fold_icon_bounds.origin, fold_icon_bounds.size.into(), window, cx, ); icon_layout .icons .push((info.display_row, info.is_folded, icon)); } icon_layout } /// Paint fold icons using prepaint hitboxes. /// /// This handles: /// - Rendering fold icons (chevron-right for folded, chevron-down for expanded) /// - Mouse click handling to toggle fold state /// - Cursor style changes on hover /// - Only show icon on hover or for current line fn paint_fold_icons( &mut self, fold_icon_layout: &mut FoldIconLayout, current_row: Option, window: &mut Window, cx: &mut App, ) { let is_hovered = fold_icon_layout.line_number_hitbox.is_hovered(window); for (display_row, is_folded, icon) in fold_icon_layout.icons.iter_mut() { let is_current_line = current_row == Some(*display_row); if !is_hovered && !is_current_line && !*is_folded { continue; } icon.paint(window, cx); } } #[allow(clippy::too_many_arguments)] fn layout_lines( state: &InputState, display_text: &Rope, last_layout: &LastLayout, font_size: Pixels, runs: &[TextRun], bg_segments: &[(Range, Hsla)], whitespace_indicators: Option, window: &mut Window, ) -> Vec { let is_single_line = state.mode.is_single_line(); let buffer_lines = state.display_map.lines(); if is_single_line { let shaped_line = window.text_system().shape_line( display_text.to_string().into(), font_size, runs, None, ); let line_layout = LineLayout::new() .lines(smallvec::smallvec![shaped_line]) .with_whitespaces(whitespace_indicators); return vec![line_layout]; } // Empty to use placeholder, the placeholder is not in the wrapper map. if state.text.len() == 0 { let placeholder_text = display_text.to_string(); let mut placeholder_lines = SmallVec::new(); for (line, line_runs) in placeholder_line_runs(&placeholder_text, runs) { let shaped_line = window.text_system().shape_line( line.to_string().into(), font_size, &line_runs, None, ); placeholder_lines.push(shaped_line); } // Keep placeholder lines in a single layout to stay parallel with visible_* metadata. let line_layout = LineLayout::new() .lines(placeholder_lines) .with_whitespaces(whitespace_indicators); return vec![line_layout]; } let mut lines = Vec::with_capacity(last_layout.visible_buffer_lines.len()); // run_offset tracks position in the runs vec coordinate space (only visible line bytes). // This is separate from the visible_text offset because runs from highlight_lines // only cover visible (non-folded) lines. let mut run_offset = 0; for (vi, &buffer_line) in last_layout.visible_buffer_lines.iter().enumerate() { let line_text: String = display_text.slice_line(buffer_line).into(); let line_item = buffer_lines .get(buffer_line) .expect("line should exists in wrapper"); debug_assert_eq!(line_item.len(), line_text.len()); let mut wrapped_lines = SmallVec::with_capacity(1); for range in &line_item.wrapped_lines { let line_runs = runs_for_range(runs, run_offset, range); let line_runs = if bg_segments.is_empty() { line_runs } else { split_runs_by_bg_segments( last_layout.visible_line_byte_offsets[vi] + (range.start), &line_runs, bg_segments, ) }; let sub_line: SharedString = line_text[range.clone()].to_string().into(); let shaped_line = window .text_system() .shape_line(sub_line, font_size, &line_runs, None); wrapped_lines.push(shaped_line); } let line_layout = LineLayout::new() .lines(wrapped_lines) .with_whitespaces(whitespace_indicators.clone()); lines.push(line_layout); // +1 for the `\n` run_offset += line_text.len() + 1; } lines } /// First usize is the offset of skipped. fn highlight_lines( &mut self, visible_buffer_lines: &[usize], _visible_top: Pixels, _visible_byte_range: Range, cx: &mut App, ) -> Option, HighlightStyle)>> { let state = self.state.read(cx); let text = &state.text; let is_multi_line = state.mode.is_multi_line(); let mut styles = Vec::with_capacity(visible_buffer_lines.len()); // Helper to flush a contiguous range of lines. These ranges are disjoint, // so appending avoids repeatedly cloning and recombining prior styles. let flush_range = |start_line: usize, end_line: usize, _skip: bool, styles: &mut Vec<_>| { let byte_start = text.line_start_offset(start_line); let byte_end = if is_multi_line { // +1 for `\n` text.line_start_offset(end_line + 1) } else { text.line_end_offset(end_line) }; let range_styles = vec![(byte_start..byte_end, HighlightStyle::default())]; styles.extend(range_styles); }; // Group contiguous visible lines into ranges and call styles() once per range let mut visible_iter = visible_buffer_lines.iter().peekable(); let mut range_start: Option = None; while let Some(&line) = visible_iter.next() { // Check if this line is too long for highlighting let line_len = text.slice_line(line).len(); if line_len > MAX_HIGHLIGHT_LINE_LENGTH { // Flush any accumulated range first if let Some(start) = range_start.take() { flush_range(start, line - 1, false, &mut styles); } flush_range(line, line, true, &mut styles); continue; } range_start.get_or_insert(line); // Check if next line is contiguous, if so keep accumulating if visible_iter .peek() .map(|&&next| next == line + 1) .unwrap_or(false) { continue; } // Flush the contiguous range let start_line = range_start.take().unwrap(); flush_range(start_line, line, false, &mut styles); } Some(styles) } } pub(super) struct PrepaintState { /// The lines of entire lines. last_layout: LastLayout, /// The lines only contains the visible lines in the viewport, based on `visible_range`. /// /// The child is the soft lines. line_numbers: Option>>, /// Size of the scrollable area by entire lines. scroll_size: Size, cursor_bounds: Option>, cursor_scroll_offset: Point, /// row index (zero based), no wrap, same line as the cursor. current_row: Option, selection_path: Option>, hover_highlight_path: Option>, search_match_paths: Vec<(Path, bool)>, document_color_paths: Vec<(Path, Hsla)>, hover_definition_hitbox: Option, indent_guides_path: Option>, bounds: Bounds, /// Fold icon layout data fold_icon_layout: FoldIconLayout, // Inline completion rendering data /// Shaped ghost lines to paint after cursor row (completion lines 2+) ghost_lines: Vec, /// First line of inline completion (painted after cursor on same line) ghost_first_line: Option, ghost_lines_height: Pixels, } impl PrepaintState { /// Returns cursor bounds adjusted for scroll offset, if available. fn cursor_bounds_with_scroll(&self) -> Option> { self.cursor_bounds.map(|mut bounds| { bounds.origin.y += self.cursor_scroll_offset.y; bounds }) } } impl IntoElement for TextElement { type Element = Self; fn into_element(self) -> Self::Element { self } } /// A debug function to print points as SVG path. #[allow(unused)] fn print_points_as_svg_path(line_corners: &Vec>, points: Vec>) { for corners in line_corners { println!( "tl: ({}, {}), tr: ({}, {}), bl: ({}, {}), br: ({}, {})", corners.top_left.as_f32() as i32, corners.top_left.as_f32() as i32, corners.top_right.as_f32() as i32, corners.top_right.as_f32() as i32, corners.bottom_left.as_f32() as i32, corners.bottom_left.as_f32() as i32, corners.bottom_right.as_f32() as i32, corners.bottom_right.as_f32() as i32, ); } if !points.is_empty() { println!( "M{},{}", points[0].x.as_f32() as i32, points[0].y.as_f32() as i32 ); for p in points.iter().skip(1) { println!("L{},{}", p.x.as_f32() as i32, p.y.as_f32() as i32); } } } impl Element for TextElement { type PrepaintState = PrepaintState; type RequestLayoutState = (); fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let state = self.state.read(cx); let line_height = window.line_height(); let mut style = Style::default(); style.size.width = relative(1.).into(); if state.mode.is_multi_line() { style.flex_grow = 1.0; style.size.height = relative(1.).into(); if state.mode.is_auto_grow() { // Auto grow to let height match to rows, but not exceed max rows. let rows = state.mode.max_rows().min(state.mode.rows()); style.min_size.height = (rows * line_height).into(); } else { style.min_size.height = line_height.into(); } } else { // For single-line inputs, the minimum height should be the line height style.size.height = line_height.into(); }; (window.request_layout(style, [], cx), ()) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { let style = window.text_style(); let font = style.font(); let text_size = style.font_size.to_pixels(window.rem_size()); self.state.update(cx, |state, cx| { state.display_map.set_font(font, text_size, cx); state.display_map.ensure_text_prepared(&state.text, cx); }); let state = self.state.read(cx); let line_height = window.line_height(); let (visible_range, visible_buffer_lines, visible_top) = self.calculate_visible_range(state, line_height, bounds.size.height); let visible_start_offset = state.text.line_start_offset(visible_range.start); let visible_end_offset = state .text .line_end_offset(visible_range.end.saturating_sub(1)); let highlight_styles = self.highlight_lines( &visible_buffer_lines, visible_top, visible_start_offset..visible_end_offset, cx, ); let state = self.state.read(cx); let multi_line = state.mode.is_multi_line(); let text = state.text.clone(); let is_empty = text.len() == 0; let placeholder = self.placeholder.clone(); let text_style = window.text_style(); let fg = text_style.color; let (display_text, text_color) = if is_empty { (&Rope::from(placeholder.as_str()), cx.theme().text_muted) } else if state.masked { ( &Rope::from(MASK_CHAR.to_string().repeat(text.chars().count())), fg, ) } else { (&text, fg) }; // Calculate the width of the line numbers let (line_number_width, line_number_len) = Self::layout_line_numbers(state, &text, text_size, &text_style, window); let mut bounds = bounds; let wrap_width = if multi_line && state.soft_wrap { Some(bounds.size.width - line_number_width - RIGHT_MARGIN) } else { None }; let visible_line_byte_offsets: Vec = visible_buffer_lines .iter() .map(|&bl| state.text.line_start_offset(bl)) .collect(); // For password input (masked: true), convert byte offsets to masked display byte offsets so that // layout_match_range and position_for_index work in the correct coordinate space. let (visible_line_byte_offsets, visible_range_offset) = if state.masked { let offsets = visible_line_byte_offsets .iter() .map(|&o| masked_display_offset(&text, o)) .collect(); let range_offset = masked_display_offset(&text, visible_start_offset) ..masked_display_offset(&text, visible_end_offset); (offsets, range_offset) } else { ( visible_line_byte_offsets, visible_start_offset..visible_end_offset, ) }; let mut last_layout = LastLayout { visible_range, visible_buffer_lines, visible_line_byte_offsets, visible_top, visible_range_offset, line_height, wrap_width, line_number_width, lines: Rc::new(vec![]), cursor_bounds: None, text_align: state.text_align, content_width: bounds.size.width, }; let run = TextRun { len: display_text.len(), font: style.font(), color: text_color, background_color: None, underline: None, strikethrough: None, }; let marked_run = TextRun { len: 0, font: style.font(), color: text_color, background_color: None, underline: Some(UnderlineStyle { thickness: px(1.), color: Some(text_color), wavy: false, }), strikethrough: None, }; let runs = if !is_empty { if let Some(highlight_styles) = highlight_styles { let mut runs = Vec::with_capacity(highlight_styles.len()); runs.extend(highlight_styles.iter().map(|(range, style)| { let mut run = text_style.clone().highlight(*style).to_run(range.len()); if let Some(ime_marked_range) = &state.ime_marked_range && range.start >= ime_marked_range.start && range.end <= ime_marked_range.end { run.color = marked_run.color; run.strikethrough = marked_run.strikethrough; run.underline = marked_run.underline; } run })); runs.into_iter().filter(|run| run.len > 0).collect() } else { vec![run] } } else if let Some(ime_marked_range) = &state.ime_marked_range { // IME marked text vec![ TextRun { len: ime_marked_range.start, ..run.clone() }, TextRun { len: ime_marked_range.end - ime_marked_range.start, underline: marked_run.underline, ..run.clone() }, TextRun { len: display_text.len() - ime_marked_range.end, ..run.clone() }, ] .into_iter() .filter(|run| run.len > 0) .collect() } else { vec![run] }; let document_colors = []; // Create shaped lines for whitespace indicators before layout let whitespace_indicators = Self::layout_whitespace_indicators(state, text_size, &text_style, window, cx); let lines = Self::layout_lines( state, display_text, &last_layout, text_size, &runs, &document_colors, whitespace_indicators, window, ); let mut longest_line_width = wrap_width.unwrap_or(px(0.)); // 1. Single line // 2. Multi-line with soft wrap disabled. if state.mode.is_single_line() || !state.soft_wrap { let longest_row = state.display_map.longest_row(); let longest_line: SharedString = state.text.slice_line(longest_row).to_string().into(); longest_line_width = window .text_system() .shape_line( longest_line.clone(), text_size, &[TextRun { len: longest_line.len(), font: style.font(), color: gpui::black(), background_color: None, underline: None, strikethrough: None, }], wrap_width, ) .width; } last_layout.lines = Rc::new(lines); let (ghost_first_line, ghost_lines) = Self::layout_inline_completion( state, &last_layout.visible_range, text_size, window, cx, ); let ghost_line_count = ghost_lines.len(); let ghost_lines_height = ghost_line_count as f32 * line_height; let total_wrapped_lines = state.display_map.wrap_row_count(); let empty_bottom_height = if state.mode.is_code_editor() { bounds .size .height .half() .max(BOTTOM_MARGIN_ROWS * line_height) } else { px(0.) }; let mut scroll_size = size( if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width { longest_line_width + line_number_width + RIGHT_MARGIN } else { longest_line_width }, (total_wrapped_lines as f32 * line_height + empty_bottom_height + ghost_lines_height) .max(bounds.size.height), ); // TODO: should be add some gap to right, to convenient to focus on boundary position if last_layout.text_align == TextAlign::Right || last_layout.text_align == TextAlign::Center { scroll_size.width = longest_line_width + line_number_width; } // `position_for_index` for example // // #### text // // Hello 世界,this is GPUI component. // The GPUI Component is a collection of UI components for // GPUI framework, including Button, Input, Checkbox, Radio, // Dropdown, Tab, and more... // // wrap_width: 444px, line_height: 20px // // #### lines[0] // // | index | pos | line | // |-------|------------------|------| // | 5 | (37 px, 0.0) | 0 | // | 38 | (261.7 px, 20.0) | 0 | // | 40 | None | - | // // #### lines[1] // // | index | position | line | // |-------|-----------------------|------| // | 5 | (43.578125 px, 0.0) | 0 | // | 56 | (422.21094 px, 0.0) | 0 | // | 57 | (11.6328125 px, 20.0) | 1 | // | 114 | (429.85938 px, 20.0) | 1 | // | 115 | (11.3125 px, 40.0) | 2 | // Calculate the scroll offset to keep the cursor in view // Save the unscrolled x before layout_cursor modifies bounds.origin with scroll_offset. // Fold icons and their hitboxes must use this value so they stay fixed in the gutter // regardless of horizontal scroll position. let input_bounds = bounds; let original_x = bounds.origin.x; let (cursor_bounds, cursor_scroll_offset, current_row) = self.layout_cursor(&last_layout, &mut bounds, scroll_size, window, cx); last_layout.cursor_bounds = cursor_bounds; let search_match_paths = self.layout_search_matches(&last_layout, &bounds, cx); let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx); let hover_highlight_path = self.layout_hover_highlight(&last_layout, &bounds, cx); let document_color_paths = self.layout_document_colors(&document_colors, &last_layout, &bounds, cx); let state = self.state.read(cx); let line_numbers = if state.mode.line_number() { let mut line_numbers = Vec::with_capacity(last_layout.visible_buffer_lines.len()); let other_line_runs = vec![TextRun { len: line_number_len, font: style.font(), color: cx.theme().text_muted, background_color: None, underline: None, strikethrough: None, }]; let current_line_runs = vec![TextRun { len: line_number_len, font: style.font(), color: cx.theme().text, background_color: None, underline: None, strikethrough: None, }]; // build line numbers for (line, &buffer_line) in last_layout .lines .iter() .zip(last_layout.visible_buffer_lines.iter()) { let line_no: SharedString = format!("{:>width$}", buffer_line + 1, width = line_number_len).into(); let runs = if current_row == Some(buffer_line) { ¤t_line_runs } else { &other_line_runs }; let mut sub_lines: SmallVec<[ShapedLine; 1]> = SmallVec::new(); sub_lines.push( window .text_system() .shape_line(line_no, text_size, runs, None), ); for _ in 0..line.wrapped_lines.len().saturating_sub(1) { sub_lines.push(ShapedLine::default()); } line_numbers.push(sub_lines); } Some(line_numbers) } else { None }; let indent_guides_path = self.layout_indent_guides(state, &bounds, &last_layout, &text_style, window); state .editor_scrollbar_snapshot .set(Some(EditorScrollbarSnapshot::new( input_bounds, &last_layout, scroll_size, cursor_scroll_offset, state, ))); let fold_icon_layout = self.layout_fold_icons(original_x, &bounds, &last_layout, window, cx); PrepaintState { bounds, last_layout, scroll_size, line_numbers, cursor_bounds, cursor_scroll_offset, current_row, selection_path, search_match_paths, hover_highlight_path, hover_definition_hitbox: None, document_color_paths, indent_guides_path, fold_icon_layout, ghost_first_line, ghost_lines, ghost_lines_height, } } fn paint( &mut self, _id: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, input_bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let focus_handle = self.state.read(cx).focus_handle.clone(); let show_cursor = self.state.read(cx).show_cursor(window, cx); let focused = focus_handle.is_focused(window); let bounds = prepaint.bounds; let selected_range = self.state.read(cx).selected_range; let text_align = prepaint.last_layout.text_align; window.handle_input( &focus_handle, ElementInputHandler::new(bounds, self.state.clone()), cx, ); // Set Root focused_input when self is focused if focused { let state = self.state.clone(); if Root::read(window, cx).focused_input.as_ref() != Some(&state) { Root::update(window, cx, |root, _, cx| { root.focused_input = Some(state); cx.notify(); }); } } // And reset focused_input when next_frame start window.on_next_frame({ let state = self.state.clone(); move |window, cx| { if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) { Root::update(window, cx, |root, _, cx| { root.focused_input = None; cx.notify(); }); } } }); // Paint multi line text let line_height = window.line_height(); let origin = bounds.origin; let invisible_top_padding = prepaint.last_layout.visible_top; // Paint active line let mut offset_y = px(0.); if let Some(line_numbers) = prepaint.line_numbers.as_ref() { offset_y += invisible_top_padding; // Each item is the normal lines. for (lines, _) in line_numbers .iter() .zip(prepaint.last_layout.visible_buffer_lines.iter()) { let height = line_height * lines.len() as f32; offset_y += height; } } // Paint indent guides if let Some(path) = prepaint.indent_guides_path.take() { window.paint_path(path, cx.theme().border.opacity(0.85)); } // Paint selections if window.is_window_active() { let secondary_selection = cx.theme().selection; for (path, is_active) in prepaint.search_match_paths.iter() { window.paint_path(path.clone(), secondary_selection); if *is_active { window.paint_path(path.clone(), cx.theme().selection); } } if let Some(path) = prepaint.selection_path.take() { window.paint_path(path, cx.theme().selection); } // Paint hover highlight if let Some(path) = prepaint.hover_highlight_path.take() { window.paint_path(path, secondary_selection); } } // Paint document colors for (path, color) in prepaint.document_color_paths.iter() { window.paint_path(path.clone(), *color); } // Paint text with inline completion ghost line support let mut offset_y = invisible_top_padding; let ghost_lines = &prepaint.ghost_lines; let has_ghost_lines = !ghost_lines.is_empty(); // Keep scrollbar offset always be positive,Start from the left position let scroll_offset = if text_align == TextAlign::Right { (prepaint.scroll_size.width - prepaint.bounds.size.width).max(px(0.)) } else if text_align == TextAlign::Center { (prepaint.scroll_size.width - prepaint.bounds.size.width) .half() .max(px(0.)) } else { px(0.) }; // Track the y-position of the cursor row for positioning the first line suffix let mut cursor_row_y = None; for (line, &buffer_line) in prepaint .last_layout .lines .iter() .zip(prepaint.last_layout.visible_buffer_lines.iter()) { let row = buffer_line; let line_y = origin.y + offset_y; let p = point( origin.x + prepaint.last_layout.line_number_width + (scroll_offset), line_y, ); // Paint the actual line line.paint( p, line_height, text_align, Some(prepaint.last_layout.content_width), window, cx, ); offset_y += line.size(line_height).height; if Some(row) == prepaint.current_row { cursor_row_y = Some(line_y); } // After the cursor row, paint ghost lines (which shifts subsequent content down) if has_ghost_lines && Some(row) == prepaint.current_row { let ghost_x = origin.x + prepaint.last_layout.line_number_width; for ghost_line in ghost_lines { let ghost_p = point(ghost_x, origin.y + offset_y); // Paint semi-transparent background for ghost line let ghost_bounds = Bounds::new( ghost_p, size( bounds.size.width - prepaint.last_layout.line_number_width, line_height, ), ); window.paint_quad(fill(ghost_bounds, cx.theme().surface_background)); // Paint ghost line text _ = ghost_line.paint( ghost_p, line_height, text_align, Some(prepaint.last_layout.content_width), window, cx, ); offset_y += line_height; } } } // Paint blinking cursor if focused && show_cursor && let Some(cursor_bounds) = prepaint.cursor_bounds_with_scroll() { window.paint_quad(fill(cursor_bounds, cx.theme().cursor)); } // Paint line numbers let mut offset_y = px(0.); if let Some(line_numbers) = prepaint.line_numbers.as_ref() { offset_y += invisible_top_padding; window.paint_quad(fill( Bounds { origin: input_bounds.origin, size: size( prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN, input_bounds.size.height + prepaint.ghost_lines_height, ), }, cx.theme().surface_background, )); // Each item is the normal lines. for (lines, &buffer_line) in line_numbers .iter() .zip(prepaint.last_layout.visible_buffer_lines.iter()) { let p = point(input_bounds.origin.x, origin.y + offset_y); for line in lines { _ = line.paint(p, line_height, TextAlign::Left, None, window, cx); offset_y += line_height; } // Add ghost line height after cursor row for line numbers alignment if !prepaint.ghost_lines.is_empty() && prepaint.current_row == Some(buffer_line) { offset_y += prepaint.ghost_lines_height; } } } // Paint fold icons (only visible on hover or for current line) self.paint_fold_icons( &mut prepaint.fold_icon_layout, prepaint.current_row, window, cx, ); self.state.update(cx, |state, cx| { state.last_layout = Some(prepaint.last_layout.clone()); state.last_bounds = Some(bounds); state.last_cursor = Some(state.cursor()); state.set_input_bounds(input_bounds, cx); state.last_selected_range = Some(selected_range); state.scroll_size = prepaint.scroll_size; state.update_scroll_offset(Some(prepaint.cursor_scroll_offset), cx); state.deferred_scroll_offset = None; cx.notify(); }); if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() { window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); } // Paint inline completion first line suffix (after cursor on same line) if focused && let Some(first_line) = &prepaint.ghost_first_line && let (Some(cursor_bounds), Some(cursor_row_y)) = (prepaint.cursor_bounds_with_scroll(), cursor_row_y) { let first_line_x = cursor_bounds.origin.x + cursor_bounds.size.width; let p = point(first_line_x, cursor_row_y); // Paint background to cover any existing text let bg_bounds = Bounds::new(p, size(first_line.width + px(4.), line_height)); window.paint_quad(fill(bg_bounds, cx.theme().surface_background)); // Paint first line completion text _ = first_line.paint(p, line_height, text_align, None, window, cx); } self.paint_mouse_listeners(window, cx); } } /// Split placeholder text into display lines and trim runs to each line. fn placeholder_line_runs<'a>( display_text: &'a str, runs: &[TextRun], ) -> Vec<(&'a str, Vec)> { let mut result = Vec::new(); let mut line_offset = 0; for line in display_text.split('\n') { let line_runs = runs_for_range(runs, line_offset, &(0..line.len())); debug_assert_eq!( line_runs.iter().map(|run| run.len).sum::(), line.len() ); result.push((line, line_runs)); // Advance in the whole-placeholder coordinate space, including the separator. line_offset += line.len() + 1; } result } /// Get the runs for the given range. /// /// The range is the byte range of the wrapped line. pub(super) fn runs_for_range( runs: &[TextRun], line_offset: usize, range: &Range, ) -> Vec { let mut result = vec![]; let range = (line_offset + range.start)..(line_offset + range.end); let mut cursor = 0; for run in runs { let run_start = cursor; let run_end = cursor + run.len; if run_end <= range.start { cursor = run_end; continue; } if run_start >= range.end { break; } let start = range.start.max(run_start) - run_start; let end = range.end.min(run_end) - run_start; let len = end - start; if len > 0 { result.push(TextRun { len, ..run.clone() }); } cursor = run_end; } result } fn split_runs_by_bg_segments( start_offset: usize, runs: &[TextRun], bg_segments: &[(Range, Hsla)], ) -> Vec { let mut result = vec![]; let mut cursor = start_offset; for run in runs { let mut run_start = cursor; let run_end = cursor + run.len; for (bg_range, bg_color) in bg_segments { if run_end <= bg_range.start || run_start >= bg_range.end { continue; } // Overlap exists if run_start < bg_range.start { // Add the part before the background range result.push(TextRun { len: bg_range.start - run_start, ..run.clone() }); } // Add the overlapping part with background color let overlap_start = run_start.max(bg_range.start); let overlap_end = run_end.min(bg_range.end); let text_color = if bg_color.l >= 0.5 { gpui::black() } else { gpui::white() }; let run_len = overlap_end.saturating_sub(overlap_start); if run_len > 0 { result.push(TextRun { len: run_len, color: text_color, ..run.clone() }); cursor = bg_range.end; run_start = cursor; } } if run_end > cursor { // Add the part after the background range result.push(TextRun { len: run_end - cursor, ..run.clone() }); } cursor = run_end; } result } #[cfg(test)] mod tests { use super::*; #[test] fn test_editor_scrollbar_layout_uses_current_scroll_size() { let input_bounds = Bounds::new(point(px(10.), px(20.)), size(px(300.), px(80.))); let paddings = Edges { top: px(2.), right: px(3.), bottom: px(5.), left: px(7.), }; let layout = EditorScrollbarLayout::new(input_bounds, px(40.), size(px(1000.), px(200.)), paddings); assert_eq!( layout.bounds, Bounds::new(point(px(47.), px(18.)), size(px(266.), px(87.))) ); assert_eq!(layout.scroll_size, size(px(976.), px(200.))); let layout_without_gutter = EditorScrollbarLayout::new(input_bounds, px(0.), size(px(500.), px(120.)), paddings); assert_eq!( layout_without_gutter.bounds, Bounds::new(point(px(10.), px(18.)), size(px(303.), px(87.))) ); assert_eq!(layout_without_gutter.scroll_size, size(px(513.), px(120.))); } #[test] fn test_auto_grow_scroll_offset_is_clamped_to_current_viewport() { let mode = InputMode::auto_grow(3, 8); assert_eq!( clamp_auto_grow_vertical_scroll_offset(&mode, px(-260.), px(340.), px(160.)), px(-180.) ); assert_eq!( clamp_auto_grow_vertical_scroll_offset(&mode, px(-40.), px(340.), px(160.)), px(-40.) ); assert_eq!( clamp_auto_grow_vertical_scroll_offset(&mode, px(20.), px(340.), px(160.)), px(0.) ); let plain_text = InputMode::plain_text().multi_line(true); assert_eq!( clamp_auto_grow_vertical_scroll_offset(&plain_text, px(-260.), px(340.), px(160.)), px(-260.) ); } #[test] fn test_runs_for_range() { let run = TextRun { len: 0, font: gpui::font(".SystemUIFont"), color: gpui::black(), background_color: None, underline: None, strikethrough: None, }; // use hello this-is-test let runs = vec![ // use TextRun { len: 3, ..run.clone() }, // \s TextRun { len: 1, ..run.clone() }, // hello TextRun { len: 5, ..run.clone() }, // \s TextRun { len: 1, ..run.clone() }, // this-is-test TextRun { len: 12, ..run.clone() }, ]; #[track_caller] fn assert_runs(actual: Vec, expected: &[usize]) { let left = actual.iter().map(|run| run.len).collect::>(); assert_eq!(left, expected); } assert_runs(runs_for_range(&runs, 0, &(0..0)), &[]); assert_runs(runs_for_range(&runs, 0, &(0..100)), &[3, 1, 5, 1, 12]); assert_runs(runs_for_range(&runs, 0, &(0..6)), &[3, 1, 2]); assert_runs(runs_for_range(&runs, 0, &(1..6)), &[2, 1, 2]); assert_runs(runs_for_range(&runs, 0, &(3..10)), &[1, 5, 1]); assert_runs(runs_for_range(&runs, 0, &(5..8)), &[3]); assert_runs(runs_for_range(&runs, 3, &(0..3)), &[1, 2]); assert_runs(runs_for_range(&runs, 3, &(2..10)), &[4, 1, 3]); assert_runs(runs_for_range(&runs, 9, &(0..8)), &[1, 7]); } #[test] fn test_placeholder_line_runs() { let run = TextRun { len: 0, font: gpui::font(".SystemUIFont"), color: gpui::black(), background_color: None, underline: None, strikethrough: None, }; let runs = vec![ TextRun { len: 2, ..run.clone() }, TextRun { len: 2, ..run.clone() }, TextRun { len: 1, ..run }, ]; let placeholder_runs = placeholder_line_runs("ab\n\nc", &runs); let lines = placeholder_runs .iter() .map(|(line, _)| *line) .collect::>(); assert_eq!(lines, vec!["ab", "", "c"]); let run_lengths = placeholder_runs .iter() .map(|(_, line_runs)| line_runs.iter().map(|run| run.len).collect::>()) .collect::>(); assert_eq!(run_lengths, vec![vec![2], vec![], vec![1]]); } #[test] fn test_split_runs_by_bg_segments() { let run = TextRun { len: 0, font: gpui::font(".SystemUIFont"), color: gpui::blue(), background_color: None, underline: None, strikethrough: None, }; let runs = vec![ TextRun { len: 5, ..run.clone() }, TextRun { len: 7, ..run.clone() }, TextRun { len: 24, ..run.clone() }, ]; let bg_segments = vec![(8..12, gpui::red()), (12..18, gpui::blue())]; let result = split_runs_by_bg_segments(5, &runs, &bg_segments); assert_eq!( result.iter().map(|run| run.len).collect::>(), vec![3, 2, 2, 5, 1, 23] ); assert_eq!(result[0].color, gpui::blue()); assert_eq!(result[1].color, gpui::black()); assert_eq!(result[2].color, gpui::black()); assert_eq!(result[3].color, gpui::black()); assert_eq!(result[4].color, gpui::black()); assert_eq!(result[5].color, gpui::blue()); } }