use super::{ display_map::{BlockContext, ToDisplayPoint}, Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN, }; use crate::{ display_map::{BlockStyle, DisplaySnapshot, TransformBlock}, hover_popover::HoverAt, link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink}, mouse_context_menu::DeployMouseContextMenu, EditorStyle, }; use clock::ReplicaId; use collections::{BTreeMap, HashMap}; use gpui::{ color::Color, elements::*, fonts::{HighlightStyle, Underline}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, PathBuilder, }, json::{self, ToJson}, platform::CursorStyle, text_layout::{self, Line, RunStyle, TextLayoutCache}, AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent, LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; use language::{Bias, DiagnosticSeverity, Selection}; use project::ProjectPath; use settings::Settings; use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, fmt::Write, iter, ops::Range, }; const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; const HOVER_POPOVER_GAP: f32 = 10.; struct SelectionLayout { head: DisplayPoint, range: Range, } impl SelectionLayout { fn new( selection: Selection, line_mode: bool, map: &DisplaySnapshot, ) -> Self { if line_mode { let selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); let point_range = map.expand_to_line(selection.range()); Self { head: selection.head().to_display_point(map), range: point_range.start.to_display_point(map) ..point_range.end.to_display_point(map), } } else { let selection = selection.map(|p| p.to_display_point(map)); Self { head: selection.head(), range: selection.range(), } } } } pub struct EditorElement { view: WeakViewHandle, style: EditorStyle, cursor_shape: CursorShape, } impl EditorElement { pub fn new( view: WeakViewHandle, style: EditorStyle, cursor_shape: CursorShape, ) -> Self { Self { view, style, cursor_shape, } } fn view<'a>(&self, cx: &'a AppContext) -> &'a Editor { self.view.upgrade(cx).unwrap().read(cx) } fn update_view(&self, cx: &mut MutableAppContext, f: F) -> T where F: FnOnce(&mut Editor, &mut ViewContext) -> T, { self.view.upgrade(cx).unwrap().update(cx, f) } fn snapshot(&self, cx: &mut MutableAppContext) -> EditorSnapshot { self.update_view(cx, |view, cx| view.snapshot(cx)) } fn mouse_down( &self, position: Vector2F, cmd: bool, alt: bool, shift: bool, mut click_count: usize, layout: &mut LayoutState, paint: &mut PaintState, cx: &mut EventContext, ) -> bool { if cmd && paint.text_bounds.contains_point(position) { let (point, overshoot) = paint.point_for_position(&self.snapshot(cx), layout, position); if overshoot.is_zero() { cx.dispatch_action(GoToFetchedDefinition { point }); return true; } } if paint.gutter_bounds.contains_point(position) { click_count = 3; // Simulate triple-click when clicking the gutter to select lines } else if !paint.text_bounds.contains_point(position) { return false; } let snapshot = self.snapshot(cx.app); let (position, overshoot) = paint.point_for_position(&snapshot, layout, position); if shift && alt { cx.dispatch_action(Select(SelectPhase::BeginColumnar { position, overshoot: overshoot.column(), })); } else if shift { cx.dispatch_action(Select(SelectPhase::Extend { position, click_count, })); } else { cx.dispatch_action(Select(SelectPhase::Begin { position, add: alt, click_count, })); } true } fn mouse_right_down( &self, position: Vector2F, layout: &mut LayoutState, paint: &mut PaintState, cx: &mut EventContext, ) -> bool { if !paint.text_bounds.contains_point(position) { return false; } let snapshot = self.snapshot(cx.app); let (point, _) = paint.point_for_position(&snapshot, layout, position); cx.dispatch_action(DeployMouseContextMenu { position, point }); true } fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool { if self.view(cx.app.as_ref()).is_selecting() { cx.dispatch_action(Select(SelectPhase::End)); true } else { false } } fn mouse_dragged( &self, position: Vector2F, layout: &mut LayoutState, paint: &mut PaintState, cx: &mut EventContext, ) -> bool { let view = self.view(cx.app.as_ref()); if view.is_selecting() { let rect = paint.text_bounds; let mut scroll_delta = Vector2F::zero(); let vertical_margin = layout.line_height.min(rect.height() / 3.0); let top = rect.origin_y() + vertical_margin; let bottom = rect.lower_left().y() - vertical_margin; if position.y() < top { scroll_delta.set_y(-scale_vertical_mouse_autoscroll_delta(top - position.y())) } if position.y() > bottom { scroll_delta.set_y(scale_vertical_mouse_autoscroll_delta(position.y() - bottom)) } let horizontal_margin = layout.line_height.min(rect.width() / 3.0); let left = rect.origin_x() + horizontal_margin; let right = rect.upper_right().x() - horizontal_margin; if position.x() < left { scroll_delta.set_x(-scale_horizontal_mouse_autoscroll_delta( left - position.x(), )) } if position.x() > right { scroll_delta.set_x(scale_horizontal_mouse_autoscroll_delta( position.x() - right, )) } let snapshot = self.snapshot(cx.app); let (position, overshoot) = paint.point_for_position(&snapshot, layout, position); cx.dispatch_action(Select(SelectPhase::Update { position, overshoot: overshoot.column(), scroll_position: (snapshot.scroll_position() + scroll_delta) .clamp(Vector2F::zero(), layout.scroll_max), })); true } else { false } } fn mouse_moved( &self, position: Vector2F, cmd: bool, layout: &LayoutState, paint: &PaintState, cx: &mut EventContext, ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu let point = if paint.text_bounds.contains_point(position) { let (point, overshoot) = paint.point_for_position(&self.snapshot(cx), layout, position); if overshoot.is_zero() { Some(point) } else { None } } else { None }; cx.dispatch_action(UpdateGoToDefinitionLink { point, cmd_held: cmd, }); if paint .context_menu_bounds .map_or(false, |context_menu_bounds| { context_menu_bounds.contains_point(position) }) { return false; } if paint .hover_popover_bounds .iter() .any(|hover_bounds| hover_bounds.contains_point(position)) { return false; } cx.dispatch_action(HoverAt { point }); true } fn key_down(&self, input: Option<&str>, cx: &mut EventContext) -> bool { let view = self.view.upgrade(cx.app).unwrap(); if view.is_focused(cx.app) { if let Some(input) = input { cx.dispatch_action(Input(input.to_string())); true } else { false } } else { false } } fn modifiers_changed(&self, cmd: bool, cx: &mut EventContext) -> bool { cx.dispatch_action(CmdChanged { cmd_down: cmd }); false } fn scroll( &self, position: Vector2F, mut delta: Vector2F, precise: bool, layout: &mut LayoutState, paint: &mut PaintState, cx: &mut EventContext, ) -> bool { if !paint.bounds.contains_point(position) { return false; } let snapshot = self.snapshot(cx.app); let max_glyph_width = layout.em_width; if !precise { delta *= vec2f(max_glyph_width, layout.line_height); } let scroll_position = snapshot.scroll_position(); let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width; let y = (scroll_position.y() * layout.line_height - delta.y()) / layout.line_height; let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), layout.scroll_max); cx.dispatch_action(Scroll(scroll_position)); true } fn paint_background( &self, gutter_bounds: RectF, text_bounds: RectF, layout: &LayoutState, cx: &mut PaintContext, ) { let bounds = gutter_bounds.union_rect(text_bounds); let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height; let editor = self.view(cx.app); cx.scene.push_quad(Quad { bounds: gutter_bounds, background: Some(self.style.gutter_background), border: Border::new(0., Color::transparent_black()), corner_radius: 0., }); cx.scene.push_quad(Quad { bounds: text_bounds, background: Some(self.style.background), border: Border::new(0., Color::transparent_black()), corner_radius: 0., }); if let EditorMode::Full = editor.mode { 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; while active_rows.peek().map_or(false, |r| { *r.0 == end_row + 1 && r.1 == contains_non_empty_selection }) { active_rows.next().unwrap(); end_row += 1; } if !contains_non_empty_selection { let origin = vec2f( bounds.origin_x(), bounds.origin_y() + (layout.line_height * *start_row as f32) - scroll_top, ); let size = vec2f( bounds.width(), layout.line_height * (end_row - start_row + 1) as f32, ); cx.scene.push_quad(Quad { bounds: RectF::new(origin, size), background: Some(self.style.active_line_background), border: Border::default(), corner_radius: 0., }); } } if let Some(highlighted_rows) = &layout.highlighted_rows { let origin = vec2f( bounds.origin_x(), bounds.origin_y() + (layout.line_height * highlighted_rows.start as f32) - scroll_top, ); let size = vec2f( bounds.width(), layout.line_height * highlighted_rows.len() as f32, ); cx.scene.push_quad(Quad { bounds: RectF::new(origin, size), background: Some(self.style.highlighted_line_background), border: Border::default(), corner_radius: 0., }); } } } fn paint_gutter( &mut self, bounds: RectF, visible_bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext, ) { let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height; for (ix, line) in layout.line_number_layouts.iter().enumerate() { if let Some(line) = line { let line_origin = bounds.origin() + vec2f( bounds.width() - line.width() - layout.gutter_padding, ix as f32 * layout.line_height - (scroll_top % layout.line_height), ); line.paint(line_origin, visible_bounds, layout.line_height, cx); } } if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { let mut x = bounds.width() - layout.gutter_padding; let mut y = *row as f32 * layout.line_height - scroll_top; x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.; y += (layout.line_height - indicator.size().y()) / 2.; indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); } } fn paint_text( &mut self, bounds: RectF, visible_bounds: RectF, layout: &mut LayoutState, paint: &mut PaintState, cx: &mut PaintContext, ) { let view = self.view(cx.app); let style = &self.style; let local_replica_id = view.replica_id(cx); let scroll_position = layout.snapshot.scroll_position(); let start_row = scroll_position.y() as u32; let scroll_top = scroll_position.y() * layout.line_height; let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen let max_glyph_width = layout.em_width; let scroll_left = scroll_position.x() * max_glyph_width; let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); cx.scene.push_layer(Some(bounds)); cx.scene.push_cursor_region(CursorRegion { bounds, style: if !view.link_go_to_definition_state.definitions.is_empty() { CursorStyle::PointingHand } else { CursorStyle::IBeam }, }); for (range, color) in &layout.highlighted_ranges { self.paint_highlighted_range( range.clone(), start_row, end_row, *color, 0., 0.15 * layout.line_height, layout, content_origin, scroll_top, scroll_left, bounds, cx, ); } let mut cursors = SmallVec::<[Cursor; 32]>::new(); for (replica_id, selections) in &layout.selections { let selection_style = style.replica_selection_style(*replica_id); let corner_radius = 0.15 * layout.line_height; for selection in selections { self.paint_highlighted_range( selection.range.clone(), start_row, end_row, selection_style.selection, corner_radius, corner_radius * 2., layout, content_origin, scroll_top, scroll_left, bounds, cx, ); if view.show_local_cursors() || *replica_id != local_replica_id { let cursor_position = selection.head; if (start_row..end_row).contains(&cursor_position.row()) { let cursor_row_layout = &layout.line_layouts[(cursor_position.row() - start_row) as usize]; let cursor_column = cursor_position.column() as usize; let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); let mut block_width = cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; if block_width == 0.0 { block_width = layout.em_width; } let block_text = if let CursorShape::Block = self.cursor_shape { layout.snapshot.chars_at(cursor_position).next().and_then( |character| { let font_id = cursor_row_layout.font_for_index(cursor_column)?; let text = character.to_string(); Some(cx.text_layout_cache.layout_str( &text, cursor_row_layout.font_size(), &[( text.len(), RunStyle { font_id, color: style.background, underline: Default::default(), }, )], )) }, ) } else { None }; let x = cursor_character_x - scroll_left; let y = cursor_position.row() as f32 * layout.line_height - scroll_top; cursors.push(Cursor { color: selection_style.cursor, block_width, origin: vec2f(x, y), line_height: layout.line_height, shape: self.cursor_shape, block_text, }); } } } } if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) { // Draw glyphs for (ix, line) in layout.line_layouts.iter().enumerate() { let row = start_row + ix as u32; line.paint( content_origin + vec2f(-scroll_left, row as f32 * layout.line_height - scroll_top), visible_text_bounds, layout.line_height, cx, ); } } cx.scene.push_layer(Some(bounds)); for cursor in cursors { cursor.paint(content_origin, cx); } cx.scene.pop_layer(); if let Some((position, context_menu)) = layout.context_menu.as_mut() { cx.scene.push_stacking_context(None); let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize]; let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; let y = (position.row() + 1) as f32 * layout.line_height - scroll_top; let mut list_origin = content_origin + vec2f(x, y); let list_width = context_menu.size().x(); let list_height = context_menu.size().y(); // Snap the right edge of the list to the right edge of the window if // its horizontal bounds overflow. if list_origin.x() + list_width > cx.window_size.x() { list_origin.set_x((cx.window_size.x() - list_width).max(0.)); } if list_origin.y() + list_height > bounds.max_y() { list_origin.set_y(list_origin.y() - layout.line_height - list_height); } context_menu.paint( list_origin, RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor cx, ); paint.context_menu_bounds = Some(RectF::new(list_origin, context_menu.size())); cx.scene.pop_stacking_context(); } if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { cx.scene.push_stacking_context(None); // This is safe because we check on layout whether the required row is available let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize]; // Minimum required size: Take the first popover, and add 1.5 times the minimum popover // height. This is the size we will use to decide whether to render popovers above or below // the hovered line. let first_size = hover_popovers[0].size(); let height_to_reserve = first_size.y() + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.line_height; // Compute Hovered Point let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; let y = position.row() as f32 * layout.line_height - scroll_top; let hovered_point = content_origin + vec2f(x, y); paint.hover_popover_bounds.clear(); if hovered_point.y() - height_to_reserve > 0.0 { // There is enough space above. Render popovers above the hovered point let mut current_y = hovered_point.y(); for hover_popover in hover_popovers { let size = hover_popover.size(); let mut popover_origin = vec2f(hovered_point.x(), current_y - size.y()); let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); if x_out_of_bounds < 0.0 { popover_origin.set_x(popover_origin.x() + x_out_of_bounds); } hover_popover.paint( popover_origin, RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor cx, ); paint.hover_popover_bounds.push( RectF::new(popover_origin, hover_popover.size()) .dilate(Vector2F::new(0., 5.)), ); current_y = popover_origin.y() - HOVER_POPOVER_GAP; } } else { // There is not enough space above. Render popovers below the hovered point let mut current_y = hovered_point.y() + layout.line_height; for hover_popover in hover_popovers { let size = hover_popover.size(); let mut popover_origin = vec2f(hovered_point.x(), current_y); let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); if x_out_of_bounds < 0.0 { popover_origin.set_x(popover_origin.x() + x_out_of_bounds); } hover_popover.paint( popover_origin, RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor cx, ); paint.hover_popover_bounds.push( RectF::new(popover_origin, hover_popover.size()) .dilate(Vector2F::new(0., 5.)), ); current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP; } } cx.scene.pop_stacking_context(); } cx.scene.pop_layer(); } fn paint_highlighted_range( &self, range: Range, start_row: u32, end_row: u32, color: Color, corner_radius: f32, line_end_overshoot: f32, layout: &LayoutState, content_origin: Vector2F, scroll_top: f32, scroll_left: f32, bounds: RectF, cx: &mut PaintContext, ) { 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() + 1, end_row) }; let highlighted_range = HighlightedRange { color, line_height: layout.line_height, corner_radius, start_y: content_origin.y() + row_range.start as f32 * layout.line_height - scroll_top, lines: row_range .into_iter() .map(|row| { let line_layout = &layout.line_layouts[(row - start_row) as usize]; HighlightedRangeLine { start_x: if row == range.start.row() { content_origin.x() + line_layout.x_for_index(range.start.column() as usize) - scroll_left } else { content_origin.x() - scroll_left }, end_x: if row == range.end.row() { content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left } else { content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left }, } }) .collect(), }; highlighted_range.paint(bounds, cx.scene); } } fn paint_blocks( &mut self, bounds: RectF, visible_bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext, ) { let scroll_position = layout.snapshot.scroll_position(); let scroll_left = scroll_position.x() * layout.em_width; let scroll_top = scroll_position.y() * layout.line_height; for block in &mut layout.blocks { let mut origin = bounds.origin() + vec2f(0., block.row as f32 * layout.line_height - scroll_top); if !matches!(block.style, BlockStyle::Sticky) { origin += vec2f(-scroll_left, 0.); } block.element.paint(origin, visible_bounds, cx); } } fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &LayoutContext) -> f32 { let digit_count = (snapshot.max_buffer_row() as f32).log10().floor() as usize + 1; let style = &self.style; cx.text_layout_cache .layout_str( "1".repeat(digit_count).as_str(), style.text.font_size, &[( digit_count, RunStyle { font_id: style.text.font_id, color: Color::black(), underline: Default::default(), }, )], ) .width() } fn layout_line_numbers( &self, rows: Range, active_rows: &BTreeMap, snapshot: &EditorSnapshot, cx: &LayoutContext, ) -> Vec> { let style = &self.style; let include_line_numbers = snapshot.mode == EditorMode::Full; let mut line_number_layouts = Vec::with_capacity(rows.len()); let mut line_number = String::new(); for (ix, row) in snapshot .buffer_rows(rows.start) .take((rows.end - rows.start) as usize) .enumerate() { let display_row = rows.start + ix as u32; let color = if active_rows.contains_key(&display_row) { style.line_number_active } else { style.line_number }; if let Some(buffer_row) = row { if include_line_numbers { line_number.clear(); write!(&mut line_number, "{}", buffer_row + 1).unwrap(); line_number_layouts.push(Some(cx.text_layout_cache.layout_str( &line_number, style.text.font_size, &[( line_number.len(), RunStyle { font_id: style.text.font_id, color, underline: Default::default(), }, )], ))); } } else { line_number_layouts.push(None); } } line_number_layouts } fn layout_lines( &mut self, rows: Range, snapshot: &EditorSnapshot, cx: &LayoutContext, ) -> Vec { if rows.start >= rows.end { return Vec::new(); } // When the editor is empty and unfocused, then show the placeholder. if snapshot.is_empty() && !snapshot.is_focused() { let placeholder_style = self .style .placeholder_text .as_ref() .unwrap_or_else(|| &self.style.text); let placeholder_text = snapshot.placeholder_text(); let placeholder_lines = placeholder_text .as_ref() .map_or("", AsRef::as_ref) .split('\n') .skip(rows.start as usize) .chain(iter::repeat("")) .take(rows.len()); return placeholder_lines .map(|line| { cx.text_layout_cache.layout_str( line, placeholder_style.font_size, &[( line.len(), RunStyle { font_id: placeholder_style.font_id, color: placeholder_style.color, underline: Default::default(), }, )], ) }) .collect(); } else { let style = &self.style; let chunks = snapshot.chunks(rows.clone(), true).map(|chunk| { let mut highlight_style = chunk .syntax_highlight_id .and_then(|id| id.style(&style.syntax)); if let Some(chunk_highlight) = chunk.highlight_style { if let Some(highlight_style) = highlight_style.as_mut() { highlight_style.highlight(chunk_highlight); } else { highlight_style = Some(chunk_highlight); } } let mut diagnostic_highlight = HighlightStyle::default(); if chunk.is_unnecessary { diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); } if let Some(severity) = chunk.diagnostic_severity { // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { let diagnostic_style = super::diagnostic_style(severity, true, style); diagnostic_highlight.underline = Some(Underline { color: Some(diagnostic_style.message.text.color), thickness: 1.0.into(), squiggly: true, }); } } if let Some(highlight_style) = highlight_style.as_mut() { highlight_style.highlight(diagnostic_highlight); } else { highlight_style = Some(diagnostic_highlight); } (chunk.text, highlight_style) }); layout_highlighted_chunks( chunks, &style.text, &cx.text_layout_cache, &cx.font_cache, MAX_LINE_LEN, rows.len() as usize, ) } } fn layout_blocks( &mut self, rows: Range, snapshot: &EditorSnapshot, editor_width: f32, scroll_width: f32, gutter_padding: f32, gutter_width: f32, em_width: f32, text_x: f32, line_height: f32, style: &EditorStyle, line_layouts: &[text_layout::Line], cx: &mut LayoutContext, ) -> (f32, Vec) { let editor = if let Some(editor) = self.view.upgrade(cx) { editor } else { return Default::default(); }; let tooltip_style = cx.global::().theme.tooltip.clone(); let scroll_x = snapshot.scroll_position.x(); let (fixed_blocks, non_fixed_blocks) = snapshot .blocks_in_range(rows.clone()) .partition::, _>(|(_, block)| match block { TransformBlock::ExcerptHeader { .. } => false, TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, }); let mut render_block = |block: &TransformBlock, width: f32| { let mut element = match block { TransformBlock::Custom(block) => { let align_to = block .position() .to_point(&snapshot.buffer_snapshot) .to_display_point(snapshot); let anchor_x = text_x + if rows.contains(&align_to.row()) { line_layouts[(align_to.row() - rows.start) as usize] .x_for_index(align_to.column() as usize) } else { layout_line(align_to.row(), snapshot, style, cx.text_layout_cache) .x_for_index(align_to.column() as usize) }; cx.render(&editor, |_, cx| { block.render(&mut BlockContext { cx, anchor_x, gutter_padding, line_height, scroll_x, gutter_width, em_width, }) }) } TransformBlock::ExcerptHeader { key, buffer, range, starts_new_buffer, .. } => { let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { let jump_position = range .primary .as_ref() .map_or(range.context.start, |primary| primary.start); let jump_action = crate::Jump { path: ProjectPath { worktree_id: file.worktree_id(cx), path: file.path.clone(), }, position: language::ToPoint::to_point(&jump_position, buffer), anchor: jump_position, }; enum JumpIcon {} cx.render(&editor, |_, cx| { MouseEventHandler::new::(*key, cx, |state, _| { let style = style.jump_icon.style_for(state, false); Svg::new("icons/arrow_up_right_8.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) .aligned() .contained() .with_style(style.container) .constrained() .with_width(style.button_width) .with_height(style.button_width) .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(jump_action.clone()) }) .with_tooltip::( *key, "Jump to Buffer".to_string(), Some(Box::new(crate::OpenExcerpts)), tooltip_style.clone(), cx, ) .aligned() .flex_float() .boxed() }) }); if *starts_new_buffer { let style = &self.style.diagnostic_path_header; let font_size = (style.text_scale_factor * self.style.text.font_size).round(); let mut filename = None; let mut parent_path = None; if let Some(file) = buffer.file() { let path = file.path(); filename = path.file_name().map(|f| f.to_string_lossy().to_string()); parent_path = path.parent().map(|p| p.to_string_lossy().to_string() + "/"); } Flex::row() .with_child( Label::new( filename.unwrap_or_else(|| "untitled".to_string()), style.filename.text.clone().with_font_size(font_size), ) .contained() .with_style(style.filename.container) .aligned() .boxed(), ) .with_children(parent_path.map(|path| { Label::new(path, style.path.text.clone().with_font_size(font_size)) .contained() .with_style(style.path.container) .aligned() .boxed() })) .with_children(jump_icon) .contained() .with_style(style.container) .with_padding_left(gutter_padding) .with_padding_right(gutter_padding) .expanded() .named("path header block") } else { let text_style = self.style.text.clone(); Flex::row() .with_child(Label::new("…".to_string(), text_style).boxed()) .with_children(jump_icon) .contained() .with_padding_left(gutter_padding) .with_padding_right(gutter_padding) .expanded() .named("collapsed context") } } }; element.layout( SizeConstraint { min: Vector2F::zero(), max: vec2f(width, block.height() as f32 * line_height), }, cx, ); element }; let mut fixed_block_max_width = 0f32; let mut blocks = Vec::new(); for (row, block) in fixed_blocks { let element = render_block(block, f32::INFINITY); fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width); blocks.push(BlockLayout { row, element, style: BlockStyle::Fixed, }); } for (row, block) in non_fixed_blocks { let style = match block { TransformBlock::Custom(block) => block.style(), TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, }; let width = match style { BlockStyle::Sticky => editor_width, BlockStyle::Flex => editor_width .max(fixed_block_max_width) .max(gutter_width + scroll_width), BlockStyle::Fixed => unreachable!(), }; let element = render_block(block, width); blocks.push(BlockLayout { row, element, style, }); } ( scroll_width.max(fixed_block_max_width - gutter_width), blocks, ) } } impl Element for EditorElement { type LayoutState = LayoutState; type PaintState = PaintState; fn layout( &mut self, constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { let mut size = constraint.max; if size.x().is_infinite() { unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); } let snapshot = self.snapshot(cx.app); let style = self.style.clone(); let line_height = style.text.line_height(cx.font_cache); let gutter_padding; let gutter_width; let gutter_margin; if snapshot.mode == EditorMode::Full { gutter_padding = style.text.em_width(cx.font_cache) * style.gutter_padding_factor; gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; gutter_margin = -style.text.descent(cx.font_cache); } else { gutter_padding = 0.0; gutter_width = 0.0; gutter_margin = 0.0; }; let text_width = size.x() - gutter_width; let em_width = style.text.em_width(cx.font_cache); let em_advance = style.text.em_advance(cx.font_cache); let overscroll = vec2f(em_width, 0.); let snapshot = self.update_view(cx.app, |view, cx| { let wrap_width = match view.soft_wrap_mode(cx) { SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance), SoftWrap::EditorWidth => { Some(text_width - gutter_margin - overscroll.x() - em_width) } SoftWrap::Column(column) => Some(column as f32 * em_advance), }; if view.set_wrap_width(wrap_width, cx) { view.snapshot(cx) } else { snapshot } }); let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height; if let EditorMode::AutoHeight { max_lines } = snapshot.mode { size.set_y( scroll_height .min(constraint.max_along(Axis::Vertical)) .max(constraint.min_along(Axis::Vertical)) .min(line_height * max_lines as f32), ) } else if let EditorMode::SingleLine = snapshot.mode { size.set_y( line_height .min(constraint.max_along(Axis::Vertical)) .max(constraint.min_along(Axis::Vertical)), ) } else if size.y().is_infinite() { size.set_y(scroll_height); } let gutter_size = vec2f(gutter_width, size.y()); let text_size = vec2f(text_width, size.y()); let (autoscroll_horizontally, mut snapshot) = self.update_view(cx.app, |view, cx| { let autoscroll_horizontally = view.autoscroll_vertically(size.y(), line_height, cx); let snapshot = view.snapshot(cx); (autoscroll_horizontally, snapshot) }); let scroll_position = snapshot.scroll_position(); // The scroll position is a fractional point, the whole number of which represents // the top of the window in terms of display rows. let start_row = scroll_position.y() as u32; let scroll_top = scroll_position.y() * line_height; // Add 1 to ensure selections bleed off screen let end_row = 1 + cmp::min( ((scroll_top + size.y()) / line_height).ceil() as u32, snapshot.max_point().row(), ); let start_anchor = if start_row == 0 { Anchor::min() } else { snapshot .buffer_snapshot .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) }; let end_anchor = if end_row > snapshot.max_point().row() { Anchor::max() } else { snapshot .buffer_snapshot .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; let mut selections: Vec<(ReplicaId, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; let mut highlighted_ranges = Vec::new(); self.update_view(cx.app, |view, cx| { let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx)); highlighted_rows = view.highlighted_rows(); let theme = cx.global::().theme.as_ref(); highlighted_ranges = view.background_highlights_in_range( start_anchor.clone()..end_anchor.clone(), &display_map, theme, ); let mut remote_selections = HashMap::default(); for (replica_id, line_mode, selection) in display_map .buffer_snapshot .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone())) { // The local selections match the leader's selections. if Some(replica_id) == view.leader_replica_id { continue; } remote_selections .entry(replica_id) .or_insert(Vec::new()) .push(SelectionLayout::new(selection, line_mode, &display_map)); } selections.extend(remote_selections); if view.show_local_selections { let mut local_selections = view .selections .disjoint_in_range(start_anchor..end_anchor, cx); local_selections.extend(view.selections.pending(cx)); for selection in &local_selections { let is_empty = selection.start == selection.end; let selection_start = snapshot.prev_line_boundary(selection.start).1; let selection_end = snapshot.next_line_boundary(selection.end).1; for row in cmp::max(selection_start.row(), start_row) ..=cmp::min(selection_end.row(), end_row) { let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); *contains_non_empty_selection |= !is_empty; } } // Render the local selections in the leader's color when following. let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx)); selections.push(( local_replica_id, local_selections .into_iter() .map(|selection| { SelectionLayout::new(selection, view.selections.line_mode, &display_map) }) .collect(), )); } }); let line_number_layouts = self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx); let mut max_visible_line_width = 0.0; let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx); for line in &line_layouts { if line.width() > max_visible_line_width { max_visible_line_width = line.width(); } } let style = self.style.clone(); let longest_line_width = layout_line( snapshot.longest_row(), &snapshot, &style, cx.text_layout_cache, ) .width(); let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x(); let em_width = style.text.em_width(cx.font_cache); let (scroll_width, blocks) = self.layout_blocks( start_row..end_row, &snapshot, size.x(), scroll_width, gutter_padding, gutter_width, em_width, gutter_width + gutter_margin, line_height, &style, &line_layouts, cx, ); let max_row = snapshot.max_point().row(); let scroll_max = vec2f( ((scroll_width - text_size.x()) / em_width).max(0.0), max_row.saturating_sub(1) as f32, ); self.update_view(cx.app, |view, cx| { let clamped = view.clamp_scroll_left(scroll_max.x()); let autoscrolled; if autoscroll_horizontally { autoscrolled = view.autoscroll_horizontally( start_row, text_size.x(), scroll_width, em_width, &line_layouts, cx, ); } else { autoscrolled = false; } if clamped || autoscrolled { snapshot = view.snapshot(cx); } }); let mut context_menu = None; let mut code_actions_indicator = None; let mut hover = None; cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| { let newest_selection_head = view .selections .newest::(cx) .head() .to_display_point(&snapshot); let style = view.style(cx); if (start_row..end_row).contains(&newest_selection_head.row()) { if view.context_menu_visible() { context_menu = view.render_context_menu(newest_selection_head, style.clone(), cx); } code_actions_indicator = view .render_code_actions_indicator(&style, cx) .map(|indicator| (newest_selection_head.row(), indicator)); } let visible_rows = start_row..start_row + line_layouts.len() as u32; hover = view.hover_state.render(&snapshot, &style, visible_rows, cx); }); if let Some((_, context_menu)) = context_menu.as_mut() { context_menu.layout( SizeConstraint { min: Vector2F::zero(), max: vec2f( cx.window_size.x() * 0.7, (12. * line_height).min((size.y() - line_height) / 2.), ), }, cx, ); } if let Some((_, indicator)) = code_actions_indicator.as_mut() { indicator.layout( SizeConstraint::strict_along(Axis::Vertical, line_height * 0.618), cx, ); } if let Some((_, hover_popovers)) = hover.as_mut() { for hover_popover in hover_popovers.iter_mut() { hover_popover.layout( SizeConstraint { min: Vector2F::zero(), max: vec2f( (120. * em_width) // Default size .min(size.x() / 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(size.y() / 2.) // Shrink to half of the editor height .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines ), }, cx, ); } } ( size, LayoutState { size, scroll_max, gutter_size, gutter_padding, text_size, gutter_margin, snapshot, active_rows, highlighted_rows, highlighted_ranges, line_layouts, line_number_layouts, blocks, line_height, em_width, em_advance, selections, context_menu, code_actions_indicator, hover_popovers: hover, }, ) } fn paint( &mut self, bounds: RectF, visible_bounds: RectF, layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { cx.scene.push_layer(Some(bounds)); let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size); let text_bounds = RectF::new( bounds.origin() + vec2f(layout.gutter_size.x(), 0.0), layout.text_size, ); let mut paint_state = PaintState { bounds, gutter_bounds, text_bounds, context_menu_bounds: None, hover_popover_bounds: Default::default(), }; self.paint_background(gutter_bounds, text_bounds, layout, cx); if layout.gutter_size.x() > 0. { self.paint_gutter(gutter_bounds, visible_bounds, layout, cx); } self.paint_text(text_bounds, visible_bounds, layout, &mut paint_state, cx); if !layout.blocks.is_empty() { cx.scene.push_layer(Some(bounds)); self.paint_blocks(bounds, visible_bounds, layout, cx); cx.scene.pop_layer(); } cx.scene.pop_layer(); paint_state } fn dispatch_event( &mut self, event: &Event, _: RectF, _: RectF, layout: &mut LayoutState, paint: &mut PaintState, cx: &mut EventContext, ) -> bool { if let Some((_, context_menu)) = &mut layout.context_menu { if context_menu.dispatch_event(event, cx) { return true; } } if let Some((_, indicator)) = &mut layout.code_actions_indicator { if indicator.dispatch_event(event, cx) { return true; } } if let Some((_, popover_elements)) = &mut layout.hover_popovers { for popover_element in popover_elements.iter_mut() { if popover_element.dispatch_event(event, cx) { return true; } } } for block in &mut layout.blocks { if block.element.dispatch_event(event, cx) { return true; } } match event { Event::MouseDown(MouseButtonEvent { button: MouseButton::Left, position, cmd, alt, shift, click_count, .. }) => self.mouse_down( *position, *cmd, *alt, *shift, *click_count, layout, paint, cx, ), Event::MouseDown(MouseButtonEvent { button: MouseButton::Right, position, .. }) => self.mouse_right_down(*position, layout, paint, cx), Event::MouseUp(MouseButtonEvent { button: MouseButton::Left, position, .. }) => self.mouse_up(*position, cx), Event::MouseMoved(MouseMovedEvent { pressed_button: Some(MouseButton::Left), position, .. }) => self.mouse_dragged(*position, layout, paint, cx), Event::ScrollWheel(ScrollWheelEvent { position, delta, precise, }) => self.scroll(*position, *delta, *precise, layout, paint, cx), Event::KeyDown(KeyDownEvent { input, .. }) => self.key_down(input.as_deref(), cx), Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => { self.modifiers_changed(*cmd, cx) } Event::MouseMoved(MouseMovedEvent { position, cmd, .. }) => { self.mouse_moved(*position, *cmd, layout, paint, cx) } _ => false, } } fn debug( &self, bounds: RectF, _: &Self::LayoutState, _: &Self::PaintState, _: &gpui::DebugContext, ) -> json::Value { json!({ "type": "BufferElement", "bounds": bounds.to_json() }) } } pub struct LayoutState { size: Vector2F, scroll_max: Vector2F, gutter_size: Vector2F, gutter_padding: f32, gutter_margin: f32, text_size: Vector2F, snapshot: EditorSnapshot, active_rows: BTreeMap, highlighted_rows: Option>, line_layouts: Vec, line_number_layouts: Vec>, blocks: Vec, line_height: f32, em_width: f32, em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, selections: Vec<(ReplicaId, Vec)>, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, hover_popovers: Option<(DisplayPoint, Vec)>, } struct BlockLayout { row: u32, element: ElementBox, style: BlockStyle, } fn layout_line( row: u32, snapshot: &EditorSnapshot, style: &EditorStyle, layout_cache: &TextLayoutCache, ) -> text_layout::Line { let mut line = snapshot.line(row); if line.len() > MAX_LINE_LEN { let mut len = MAX_LINE_LEN; while !line.is_char_boundary(len) { len -= 1; } line.truncate(len); } layout_cache.layout_str( &line, style.text.font_size, &[( snapshot.line_len(row) as usize, RunStyle { font_id: style.text.font_id, color: Color::black(), underline: Default::default(), }, )], ) } pub struct PaintState { bounds: RectF, gutter_bounds: RectF, text_bounds: RectF, context_menu_bounds: Option, hover_popover_bounds: Vec, } impl PaintState { /// Returns two display points. The first is the nearest valid /// position in the current buffer and the second is the distance to the /// nearest valid position if there was overshoot. fn point_for_position( &self, snapshot: &EditorSnapshot, layout: &LayoutState, position: Vector2F, ) -> (DisplayPoint, DisplayPoint) { let scroll_position = snapshot.scroll_position(); let position = position - self.text_bounds.origin(); let y = position.y().max(0.0).min(layout.size.y()); let row = ((y / layout.line_height) + scroll_position.y()) as u32; let row_overshoot = row.saturating_sub(snapshot.max_point().row()); let row = cmp::min(row, snapshot.max_point().row()); let line = &layout.line_layouts[(row - scroll_position.y() as u32) as usize]; let x = position.x() + (scroll_position.x() * layout.em_width); let column = if x >= 0.0 { line.index_for_x(x) .map(|ix| ix as u32) .unwrap_or_else(|| snapshot.line_len(row)) } else { 0 }; let point = snapshot.clip_point(DisplayPoint::new(row, column), Bias::Left); let mut column_overshoot = (0f32.max(x - line.width()) / layout.em_advance) as u32; column_overshoot = column_overshoot + column - point.column(); (point, DisplayPoint::new(row_overshoot, column_overshoot)) } } #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum CursorShape { Bar, Block, Underscore, } impl Default for CursorShape { fn default() -> Self { CursorShape::Bar } } #[derive(Debug)] pub struct Cursor { origin: Vector2F, block_width: f32, line_height: f32, color: Color, shape: CursorShape, block_text: Option, } impl Cursor { pub fn new( origin: Vector2F, block_width: f32, line_height: f32, color: Color, shape: CursorShape, block_text: Option, ) -> Cursor { Cursor { origin, block_width, line_height, color, shape, block_text, } } pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) { let bounds = match self.shape { CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)), CursorShape::Block => RectF::new( self.origin + origin, vec2f(self.block_width, self.line_height), ), CursorShape::Underscore => RectF::new( self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0), vec2f(self.block_width, 2.0), ), }; cx.scene.push_quad(Quad { bounds, background: Some(self.color), border: Border::new(0., Color::black()), corner_radius: 0., }); if let Some(block_text) = &self.block_text { block_text.paint(self.origin + origin, bounds, self.line_height, cx); } } } #[derive(Debug)] pub struct HighlightedRange { pub start_y: f32, pub line_height: f32, pub lines: Vec, pub color: Color, pub corner_radius: f32, } #[derive(Debug)] pub struct HighlightedRangeLine { pub start_x: f32, pub end_x: f32, } impl HighlightedRange { pub fn paint(&self, bounds: RectF, scene: &mut Scene) { 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], bounds, scene); self.paint_lines( self.start_y + self.line_height, &self.lines[1..], bounds, scene, ); } else { self.paint_lines(self.start_y, &self.lines, bounds, scene); } } fn paint_lines( &self, start_y: f32, lines: &[HighlightedRangeLine], bounds: RectF, scene: &mut Scene, ) { if lines.is_empty() { return; } let mut path = PathBuilder::new(); let first_line = lines.first().unwrap(); let last_line = lines.last().unwrap(); let first_top_left = vec2f(first_line.start_x, start_y); let first_top_right = vec2f(first_line.end_x, start_y); let curve_height = vec2f(0., self.corner_radius); let curve_width = |start_x: f32, end_x: f32| { let max = (end_x - start_x) / 2.; let width = if max < self.corner_radius { max } else { self.corner_radius }; vec2f(width, 0.) }; let top_curve_width = curve_width(first_line.start_x, first_line.end_x); path.reset(first_top_right - top_curve_width); path.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 = vec2f(line.end_x, start_y + (ix + 1) as f32 * self.line_height); if let Some((_, next_line)) = iter.peek() { let next_top_right = vec2f(next_line.end_x, bottom_right.y()); match next_top_right.x().partial_cmp(&bottom_right.x()).unwrap() { Ordering::Equal => { path.line_to(bottom_right); } Ordering::Less => { let curve_width = curve_width(next_top_right.x(), bottom_right.x()); path.line_to(bottom_right - curve_height); if self.corner_radius > 0. { path.curve_to(bottom_right - curve_width, bottom_right); } path.line_to(next_top_right + curve_width); if self.corner_radius > 0. { path.curve_to(next_top_right + curve_height, next_top_right); } } Ordering::Greater => { let curve_width = curve_width(bottom_right.x(), next_top_right.x()); path.line_to(bottom_right - curve_height); if self.corner_radius > 0. { path.curve_to(bottom_right + curve_width, bottom_right); } path.line_to(next_top_right - curve_width); if self.corner_radius > 0. { path.curve_to(next_top_right + curve_height, next_top_right); } } } } else { let curve_width = curve_width(line.start_x, line.end_x); path.line_to(bottom_right - curve_height); if self.corner_radius > 0. { path.curve_to(bottom_right - curve_width, bottom_right); } let bottom_left = vec2f(line.start_x, bottom_right.y()); path.line_to(bottom_left + curve_width); if self.corner_radius > 0. { path.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 = vec2f(last_line.start_x, start_y + self.line_height); path.line_to(second_top_left + curve_height); if self.corner_radius > 0. { path.curve_to(second_top_left + curve_width, second_top_left); } let first_bottom_left = vec2f(first_line.start_x, second_top_left.y()); path.line_to(first_bottom_left - curve_width); if self.corner_radius > 0. { path.curve_to(first_bottom_left - curve_height, first_bottom_left); } } path.line_to(first_top_left + curve_height); if self.corner_radius > 0. { path.curve_to(first_top_left + top_curve_width, first_top_left); } path.line_to(first_top_right - top_curve_width); scene.push_path(path.build(self.color, Some(bounds))); } } fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 { delta.powf(1.5) / 100.0 } fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 { delta.powf(1.2) / 300.0 } #[cfg(test)] mod tests { use std::sync::Arc; use super::*; use crate::{ display_map::{BlockDisposition, BlockProperties}, Editor, MultiBuffer, }; use settings::Settings; use util::test::sample_text; #[gpui::test] fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let (window_id, editor) = cx.add_window(Default::default(), |cx| { Editor::new(EditorMode::Full, buffer, None, None, cx) }); let element = EditorElement::new( editor.downgrade(), editor.read(cx).style(cx), CursorShape::Bar, ); let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); let mut presenter = cx.build_presenter(window_id, 30.); let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx); element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx) }); assert_eq!(layouts.len(), 6); } #[gpui::test] fn test_layout_with_placeholder_text_and_blocks(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("", cx); let (window_id, editor) = cx.add_window(Default::default(), |cx| { Editor::new(EditorMode::Full, buffer, None, None, cx) }); editor.update(cx, |editor, cx| { editor.set_placeholder_text("hello", cx); editor.insert_blocks( [BlockProperties { style: BlockStyle::Fixed, disposition: BlockDisposition::Above, height: 3, position: Anchor::min(), render: Arc::new(|_| Empty::new().boxed()), }], cx, ); // Blur the editor so that it displays placeholder text. cx.blur(); }); let mut element = EditorElement::new( editor.downgrade(), editor.read(cx).style(cx), CursorShape::Bar, ); let mut scene = Scene::new(1.0); let mut presenter = cx.build_presenter(window_id, 30.); let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx); let (size, mut state) = element.layout( SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), &mut layout_cx, ); assert_eq!(state.line_layouts.len(), 4); assert_eq!( state .line_number_layouts .iter() .map(Option::is_some) .collect::>(), &[false, false, false, true] ); // Don't panic. let bounds = RectF::new(Default::default(), size); let mut paint_cx = presenter.build_paint_context(&mut scene, bounds.size(), cx); element.paint(bounds, bounds, &mut state, &mut paint_cx); } }