From d209eab05879ddd49c4ebbb439966150f7c3b686 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Oct 2024 17:58:07 -0700 Subject: [PATCH] Combine excerpt footer and header into a single block (#19441) This simplifies rendering of excerpt headers and footers, and removes the need to store a `BlockDisposition` on these boundary blocks. It's a step toward implementing "replace blocks", which we want to use in the assistant panel. We've also cleaned up the way heights are specified for headers and footers and fixed some visual asymmetries between the "expand upward" and "expand downward" buttons. Release Notes: - N/A --------- Co-authored-by: Richard --- crates/diagnostics/src/diagnostics_tests.rs | 4 +- crates/editor/src/display_map/block_map.rs | 301 +++++----- crates/editor/src/editor.rs | 31 +- crates/editor/src/element.rs | 585 ++++++++------------ crates/editor/src/movement.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 2 + crates/repl/src/session.rs | 29 +- 7 files changed, 401 insertions(+), 553 deletions(-) diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 75bfd6415c..1daffffb4e 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -962,7 +962,6 @@ fn random_diagnostic( const FILE_HEADER: &str = "file header"; const EXCERPT_HEADER: &str = "excerpt header"; -const EXCERPT_FOOTER: &str = "excerpt footer"; fn editor_blocks( editor: &View, @@ -998,7 +997,7 @@ fn editor_blocks( .ok()? } - Block::ExcerptHeader { + Block::ExcerptBoundary { starts_new_buffer, .. } => { if *starts_new_buffer { @@ -1007,7 +1006,6 @@ fn editor_blocks( EXCERPT_HEADER.into() } } - Block::ExcerptFooter { .. } => EXCERPT_FOOTER.into(), }; Some((row, name)) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 52e0ca2486..f4ee57408b 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -5,8 +5,8 @@ use super::{ use crate::{EditorStyle, GutterDimensions}; use collections::{Bound, HashMap, HashSet}; use gpui::{AnyElement, EntityId, Pixels, WindowContext}; -use language::{BufferSnapshot, Chunk, Patch, Point}; -use multi_buffer::{Anchor, ExcerptId, ExcerptRange, MultiBufferRow, ToPoint as _}; +use language::{Chunk, Patch, Point}; +use multi_buffer::{Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, ToPoint as _}; use parking_lot::Mutex; use std::{ cell::RefCell, @@ -128,26 +128,17 @@ pub struct BlockContext<'a, 'b> { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum BlockId { Custom(CustomBlockId), - ExcerptHeader(ExcerptId), - ExcerptFooter(ExcerptId), -} - -impl From for EntityId { - fn from(value: BlockId) -> Self { - match value { - BlockId::Custom(CustomBlockId(id)) => EntityId::from(id as u64), - BlockId::ExcerptHeader(id) => id.into(), - BlockId::ExcerptFooter(id) => id.into(), - } - } + ExcerptBoundary(Option), } impl From for ElementId { fn from(value: BlockId) -> Self { match value { BlockId::Custom(CustomBlockId(id)) => ("Block", id).into(), - BlockId::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(), - BlockId::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(), + BlockId::ExcerptBoundary(next_excerpt) => match next_excerpt { + Some(id) => ("ExcerptBoundary", EntityId::from(id)).into(), + None => "LastExcerptBoundary".into(), + }, } } } @@ -156,8 +147,7 @@ impl std::fmt::Display for BlockId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Custom(id) => write!(f, "Block({id:?})"), - Self::ExcerptHeader(id) => write!(f, "ExcerptHeader({id:?})"), - Self::ExcerptFooter(id) => write!(f, "ExcerptFooter({id:?})"), + Self::ExcerptBoundary(id) => write!(f, "ExcerptHeader({id:?})"), } } } @@ -177,8 +167,7 @@ struct Transform { pub(crate) enum BlockType { Custom(CustomBlockId), - Header, - Footer, + ExcerptBoundary, } pub(crate) trait BlockLike { @@ -191,27 +180,20 @@ pub(crate) trait BlockLike { #[derive(Clone)] pub enum Block { Custom(Arc), - ExcerptHeader { - id: ExcerptId, - buffer: BufferSnapshot, - range: ExcerptRange, + ExcerptBoundary { + prev_excerpt: Option, + next_excerpt: Option, height: u32, starts_new_buffer: bool, show_excerpt_controls: bool, }, - ExcerptFooter { - id: ExcerptId, - disposition: BlockDisposition, - height: u32, - }, } impl BlockLike for Block { fn block_type(&self) -> BlockType { match self { Block::Custom(block) => BlockType::Custom(block.id), - Block::ExcerptHeader { .. } => BlockType::Header, - Block::ExcerptFooter { .. } => BlockType::Footer, + Block::ExcerptBoundary { .. } => BlockType::ExcerptBoundary, } } @@ -222,8 +204,7 @@ impl BlockLike for Block { fn priority(&self) -> usize { match self { Block::Custom(block) => block.priority, - Block::ExcerptHeader { .. } => usize::MAX, - Block::ExcerptFooter { .. } => 0, + Block::ExcerptBoundary { .. } => usize::MAX, } } } @@ -232,32 +213,36 @@ impl Block { pub fn id(&self) -> BlockId { match self { Block::Custom(block) => BlockId::Custom(block.id), - Block::ExcerptHeader { id, .. } => BlockId::ExcerptHeader(*id), - Block::ExcerptFooter { id, .. } => BlockId::ExcerptFooter(*id), + Block::ExcerptBoundary { next_excerpt, .. } => { + BlockId::ExcerptBoundary(next_excerpt.as_ref().map(|info| info.id)) + } } } fn disposition(&self) -> BlockDisposition { match self { Block::Custom(block) => block.disposition, - Block::ExcerptHeader { .. } => BlockDisposition::Above, - Block::ExcerptFooter { disposition, .. } => *disposition, + Block::ExcerptBoundary { next_excerpt, .. } => { + if next_excerpt.is_some() { + BlockDisposition::Above + } else { + BlockDisposition::Below + } + } } } pub fn height(&self) -> u32 { match self { Block::Custom(block) => block.height, - Block::ExcerptHeader { height, .. } => *height, - Block::ExcerptFooter { height, .. } => *height, + Block::ExcerptBoundary { height, .. } => *height, } } pub fn style(&self) -> BlockStyle { match self { Block::Custom(block) => block.style, - Block::ExcerptHeader { .. } => BlockStyle::Sticky, - Block::ExcerptFooter { .. } => BlockStyle::Sticky, + Block::ExcerptBoundary { .. } => BlockStyle::Sticky, } } } @@ -266,24 +251,17 @@ impl Debug for Block { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(), - Self::ExcerptHeader { - buffer, + Self::ExcerptBoundary { starts_new_buffer, - id, + next_excerpt, + prev_excerpt, .. } => f - .debug_struct("ExcerptHeader") - .field("id", &id) - .field("path", &buffer.file().map(|f| f.path())) + .debug_struct("ExcerptBoundary") + .field("prev_excerpt", &prev_excerpt) + .field("next_excerpt", &next_excerpt) .field("starts_new_buffer", &starts_new_buffer) .finish(), - Block::ExcerptFooter { - id, disposition, .. - } => f - .debug_struct("ExcerptFooter") - .field("id", &id) - .field("disposition", &disposition) - .finish(), } } } @@ -595,66 +573,62 @@ impl BlockMap { { buffer .excerpt_boundaries_in_range(range) - .flat_map(move |excerpt_boundary| { - let mut wrap_row = wrap_snapshot - .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left) - .row(); - - [ - show_excerpt_controls - .then(|| { - let disposition; - if excerpt_boundary.next.is_some() { - disposition = BlockDisposition::Above; - } else { - wrap_row = wrap_snapshot - .make_wrap_point( - Point::new( - excerpt_boundary.row.0, - buffer.line_len(excerpt_boundary.row), - ), - Bias::Left, - ) - .row(); - disposition = BlockDisposition::Below; - } - - excerpt_boundary.prev.as_ref().map(|prev| { - ( - wrap_row, - Block::ExcerptFooter { - id: prev.id, - height: excerpt_footer_height, - disposition, - }, - ) - }) - }) - .flatten(), - excerpt_boundary.next.map(|next| { - let starts_new_buffer = excerpt_boundary - .prev - .map_or(true, |prev| prev.buffer_id != next.buffer_id); - - ( - wrap_row, - Block::ExcerptHeader { - id: next.id, - buffer: next.buffer, - range: next.range, - height: if starts_new_buffer { - buffer_header_height - } else { - excerpt_header_height - }, - starts_new_buffer, - show_excerpt_controls, - }, + .filter_map(move |excerpt_boundary| { + let wrap_row; + if excerpt_boundary.next.is_some() { + wrap_row = wrap_snapshot + .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left) + .row(); + } else { + wrap_row = wrap_snapshot + .make_wrap_point( + Point::new( + excerpt_boundary.row.0, + buffer.line_len(excerpt_boundary.row), + ), + Bias::Left, ) - }), - ] + .row(); + } + + let starts_new_buffer = match (&excerpt_boundary.prev, &excerpt_boundary.next) { + (_, None) => false, + (None, Some(_)) => true, + (Some(prev), Some(next)) => prev.buffer_id != next.buffer_id, + }; + + let mut height = 0; + if excerpt_boundary.prev.is_some() { + if show_excerpt_controls { + height += excerpt_footer_height; + } + } + if excerpt_boundary.next.is_some() { + if starts_new_buffer { + height += buffer_header_height; + if show_excerpt_controls { + height += excerpt_header_height; + } + } else { + height += excerpt_header_height; + } + } + + if height == 0 { + return None; + } + + Some(( + wrap_row, + Block::ExcerptBoundary { + prev_excerpt: excerpt_boundary.prev, + next_excerpt: excerpt_boundary.next, + height, + starts_new_buffer, + show_excerpt_controls, + }, + )) }) - .flatten() } pub(crate) fn sort_blocks(blocks: &mut [(u32, B)]) { @@ -665,12 +639,9 @@ impl BlockMap { .disposition() .cmp(&block_b.disposition()) .then_with(|| match ((block_a.block_type()), (block_b.block_type())) { - (BlockType::Footer, BlockType::Footer) => Ordering::Equal, - (BlockType::Footer, _) => Ordering::Less, - (_, BlockType::Footer) => Ordering::Greater, - (BlockType::Header, BlockType::Header) => Ordering::Equal, - (BlockType::Header, _) => Ordering::Less, - (_, BlockType::Header) => Ordering::Greater, + (BlockType::ExcerptBoundary, BlockType::ExcerptBoundary) => Ordering::Equal, + (BlockType::ExcerptBoundary, _) => Ordering::Less, + (_, BlockType::ExcerptBoundary) => Ordering::Greater, (BlockType::Custom(a_id), BlockType::Custom(b_id)) => block_b .priority() .cmp(&block_a.priority()) @@ -1045,33 +1016,19 @@ impl BlockSnapshot { let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?; Some(Block::Custom(custom_block.clone())) } - BlockId::ExcerptHeader(excerpt_id) => { - let excerpt_range = buffer.range_for_excerpt::(excerpt_id)?; - let wrap_point = self - .wrap_snapshot - .make_wrap_point(excerpt_range.start, Bias::Left); - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); - cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &()); - while let Some(transform) = cursor.item() { - if let Some(block) = transform.block.as_ref() { - if block.id() == block_id { - return Some(block.clone()); - } - } else if cursor.start().0 > WrapRow(wrap_point.row()) { - break; - } - - cursor.next(&()); + BlockId::ExcerptBoundary(next_excerpt_id) => { + let wrap_point; + if let Some(next_excerpt_id) = next_excerpt_id { + let excerpt_range = buffer.range_for_excerpt::(next_excerpt_id)?; + wrap_point = self + .wrap_snapshot + .make_wrap_point(excerpt_range.start, Bias::Left); + } else { + wrap_point = self + .wrap_snapshot + .make_wrap_point(buffer.max_point(), Bias::Left); } - None - } - BlockId::ExcerptFooter(excerpt_id) => { - let excerpt_range = buffer.range_for_excerpt::(excerpt_id)?; - let wrap_point = self - .wrap_snapshot - .make_wrap_point(excerpt_range.end, Bias::Left); - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &()); while let Some(transform) = cursor.item() { @@ -1468,7 +1425,7 @@ mod tests { }; use gpui::{div, font, px, AppContext, Context as _, Element}; use language::{Buffer, Capability}; - use multi_buffer::MultiBuffer; + use multi_buffer::{ExcerptRange, MultiBuffer}; use rand::prelude::*; use settings::SettingsStore; use std::env; @@ -1724,22 +1681,20 @@ mod tests { // Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline. assert_eq!( snapshot.text(), - "\nBuff\ner 1\n\n\nBuff\ner 2\n\n\nBuff\ner 3\n" + "\n\nBuff\ner 1\n\n\n\nBuff\ner 2\n\n\n\nBuff\ner 3\n" ); let blocks: Vec<_> = snapshot .blocks_in_range(0..u32::MAX) - .map(|(row, block)| (row, block.id())) + .map(|(row, block)| (row..row + block.height(), block.id())) .collect(); assert_eq!( blocks, vec![ - (0, BlockId::ExcerptHeader(excerpt_ids[0])), - (3, BlockId::ExcerptFooter(excerpt_ids[0])), - (4, BlockId::ExcerptHeader(excerpt_ids[1])), - (7, BlockId::ExcerptFooter(excerpt_ids[1])), - (8, BlockId::ExcerptHeader(excerpt_ids[2])), - (11, BlockId::ExcerptFooter(excerpt_ids[2])) + (0..2, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header + (4..7, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // footer, path, header + (9..12, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // footer, path, header + (14..15, BlockId::ExcerptBoundary(None)), // footer ] ); } @@ -2283,13 +2238,10 @@ mod tests { #[derive(Debug, Eq, PartialEq)] enum ExpectedBlock { - ExcerptHeader { + ExcerptBoundary { height: u32, starts_new_buffer: bool, - }, - ExcerptFooter { - height: u32, - disposition: BlockDisposition, + is_last: bool, }, Custom { disposition: BlockDisposition, @@ -2303,8 +2255,7 @@ mod tests { fn block_type(&self) -> BlockType { match self { ExpectedBlock::Custom { id, .. } => BlockType::Custom(*id), - ExpectedBlock::ExcerptHeader { .. } => BlockType::Header, - ExpectedBlock::ExcerptFooter { .. } => BlockType::Footer, + ExpectedBlock::ExcerptBoundary { .. } => BlockType::ExcerptBoundary, } } @@ -2315,8 +2266,7 @@ mod tests { fn priority(&self) -> usize { match self { ExpectedBlock::Custom { priority, .. } => *priority, - ExpectedBlock::ExcerptHeader { .. } => usize::MAX, - ExpectedBlock::ExcerptFooter { .. } => 0, + ExpectedBlock::ExcerptBoundary { .. } => usize::MAX, } } } @@ -2324,17 +2274,21 @@ mod tests { impl ExpectedBlock { fn height(&self) -> u32 { match self { - ExpectedBlock::ExcerptHeader { height, .. } => *height, + ExpectedBlock::ExcerptBoundary { height, .. } => *height, ExpectedBlock::Custom { height, .. } => *height, - ExpectedBlock::ExcerptFooter { height, .. } => *height, } } fn disposition(&self) -> BlockDisposition { match self { - ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above, + ExpectedBlock::ExcerptBoundary { is_last, .. } => { + if *is_last { + BlockDisposition::Below + } else { + BlockDisposition::Above + } + } ExpectedBlock::Custom { disposition, .. } => *disposition, - ExpectedBlock::ExcerptFooter { disposition, .. } => *disposition, } } } @@ -2348,21 +2302,15 @@ mod tests { height: block.height, priority: block.priority, }, - Block::ExcerptHeader { + Block::ExcerptBoundary { height, starts_new_buffer, + next_excerpt, .. - } => ExpectedBlock::ExcerptHeader { + } => ExpectedBlock::ExcerptBoundary { height, starts_new_buffer, - }, - Block::ExcerptFooter { - height, - disposition, - .. - } => ExpectedBlock::ExcerptFooter { - height, - disposition, + is_last: next_excerpt.is_none(), }, } } @@ -2380,8 +2328,7 @@ mod tests { fn as_custom(&self) -> Option<&CustomBlock> { match self { Block::Custom(block) => Some(block), - Block::ExcerptHeader { .. } => None, - Block::ExcerptFooter { .. } => None, + Block::ExcerptBoundary { .. } => None, } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9d9cfde7b9..ba3841b4e2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -73,12 +73,12 @@ use git::blame::GitBlame; use gpui::{ div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry, - ClipboardItem, Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle, - FocusOutEvent, FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, - KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, - SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, - UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, - VisualContext, WeakFocusHandle, WeakView, WindowContext, + ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, + FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, + ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, + Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UTF16Selection, + UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, + WeakFocusHandle, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -171,7 +171,7 @@ use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSetti use crate::hover_links::find_url; use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; -pub const FILE_HEADER_HEIGHT: u32 = 1; +pub const FILE_HEADER_HEIGHT: u32 = 2; pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u32 = 1; pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; @@ -640,7 +640,6 @@ pub struct Editor { tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, previous_search_ranges: Option]>>, - file_header_size: u32, breadcrumb_header: Option, focused_block: Option, next_scroll_position: NextScrollCursorCenterTopBottom, @@ -1846,7 +1845,6 @@ impl Editor { }), merge_adjacent: true, }; - let file_header_size = if show_excerpt_controls { 3 } else { 2 }; let display_map = cx.new_model(|cx| { DisplayMap::new( buffer.clone(), @@ -1854,7 +1852,7 @@ impl Editor { font_size, None, show_excerpt_controls, - file_header_size, + FILE_HEADER_HEIGHT, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT, fold_placeholder, @@ -2038,7 +2036,6 @@ impl Editor { .restore_unsaved_buffers, blame: None, blame_subscription: None, - file_header_size, tasks: Default::default(), _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -12808,7 +12805,7 @@ impl Editor { } pub fn file_header_size(&self) -> u32 { - self.file_header_size + FILE_HEADER_HEIGHT } pub fn revert( @@ -14120,7 +14117,7 @@ pub fn diagnostic_block_renderer( let multi_line_diagnostic = diagnostic.message.contains('\n'); - let buttons = |diagnostic: &Diagnostic, block_id: BlockId| { + let buttons = |diagnostic: &Diagnostic| { if multi_line_diagnostic { v_flex() } else { @@ -14128,7 +14125,7 @@ pub fn diagnostic_block_renderer( } .when(allow_closing, |div| { div.children(diagnostic.is_primary.then(|| { - IconButton::new(("close-block", EntityId::from(block_id)), IconName::XCircle) + IconButton::new("close-block", IconName::XCircle) .icon_color(Color::Muted) .size(ButtonSize::Compact) .style(ButtonStyle::Transparent) @@ -14138,7 +14135,7 @@ pub fn diagnostic_block_renderer( })) }) .child( - IconButton::new(("copy-block", EntityId::from(block_id)), IconName::Copy) + IconButton::new("copy-block", IconName::Copy) .icon_color(Color::Muted) .size(ButtonSize::Compact) .style(ButtonStyle::Transparent) @@ -14153,7 +14150,7 @@ pub fn diagnostic_block_renderer( ) }; - let icon_size = buttons(&diagnostic, cx.block_id) + let icon_size = buttons(&diagnostic) .into_any_element() .layout_as_root(AvailableSpace::min_size(), cx); @@ -14170,7 +14167,7 @@ pub fn diagnostic_block_renderer( .w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width) .flex_shrink(), ) - .child(buttons(&diagnostic, cx.block_id)) + .child(buttons(&diagnostic)) .child(div().flex().flex_shrink_0().child( StyledText::new(text_without_backticks.clone()).with_highlights( &text_style, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a9dfe7e435..77b78d059c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -21,7 +21,8 @@ use crate::{ EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, - CURSORS_VISIBLE_FOR, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, + CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use client::ParticipantIndex; use collections::{BTreeMap, HashMap}; @@ -31,7 +32,7 @@ use gpui::{ anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg, transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, - EntityId, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length, + FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, @@ -46,7 +47,7 @@ use language::{ ChunkRendererContext, }; use lsp::DiagnosticSeverity; -use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow}; +use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow}; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, ProjectPath, @@ -1632,7 +1633,7 @@ impl EditorElement { let mut block_offset = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) { - if matches!(block, Block::ExcerptHeader { .. }) { + if matches!(block, Block::ExcerptBoundary { .. }) { found_excerpt_header = true; break; } @@ -1649,7 +1650,7 @@ impl EditorElement { let mut block_height = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) { - if matches!(block, Block::ExcerptHeader { .. }) { + if matches!(block, Block::ExcerptBoundary { .. }) { found_excerpt_header = true; } block_height += block.height(); @@ -2100,23 +2101,14 @@ impl EditorElement { .into_any_element() } - Block::ExcerptHeader { - buffer, - range, + Block::ExcerptBoundary { + prev_excerpt, + next_excerpt, + show_excerpt_controls, starts_new_buffer, height, - id, - show_excerpt_controls, .. } => { - let include_root = self - .editor - .read(cx) - .project - .as_ref() - .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - .unwrap_or_default(); - #[derive(Clone)] struct JumpData { position: Point, @@ -2125,233 +2117,227 @@ impl EditorElement { line_offset_from_top: u32, } - let jump_data = project::File::from_dyn(buffer.file()).map(|file| { - let jump_path = ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }; - let jump_anchor = range - .primary - .as_ref() - .map_or(range.context.start, |primary| primary.start); - - let excerpt_start = range.context.start; - let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - let offset_from_excerpt_start = if jump_anchor == excerpt_start { - 0 - } else { - let excerpt_start_row = - language::ToPoint::to_point(&jump_anchor, buffer).row; - jump_position.row - excerpt_start_row - }; - - let line_offset_from_top = - block_row_start.0 + *height + offset_from_excerpt_start - - snapshot - .scroll_anchor - .scroll_position(&snapshot.display_snapshot) - .y as u32; - - JumpData { - position: jump_position, - anchor: jump_anchor, - path: jump_path, - line_offset_from_top, - } - }); - let icon_offset = gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin); - let element = if *starts_new_buffer { - let path = buffer.resolve_file_path(cx, include_root); - let mut filename = None; - let mut parent_path = None; - // Can't use .and_then() because `.file_name()` and `.parent()` return references :( - if let Some(path) = path { - filename = path.file_name().map(|f| f.to_string_lossy().to_string()); - parent_path = path - .parent() - .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/")); - } + let header_padding = px(6.0); - let header_padding = px(6.0); + let mut result = v_flex().id(block_id).w_full(); - v_flex() - .id(("path excerpt header", EntityId::from(block_id))) - .w_full() - .px(header_padding) - .pt(header_padding) - .child( + if let Some(prev_excerpt) = prev_excerpt { + if *show_excerpt_controls { + result = result.child( h_flex() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .id("path header block") - .h(2. * cx.line_height()) - .px(gpui::px(12.)) - .rounded_md() - .shadow_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_subheader_background) - .justify_between() - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .child( - h_flex().gap_3().child( - h_flex() - .gap_2() - .child( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .when_some(parent_path, |then, path| { - then.child( - div() - .child(path) - .text_color(cx.theme().colors().text_muted), - ) - }), - ), - ) - .when_some(jump_data.clone(), |el, jump_data| { - el.child(Icon::new(IconName::ArrowUpRight)) - .cursor_pointer() - .tooltip(|cx| { - Tooltip::for_action("Jump to File", &OpenExcerpts, cx) - }) - .on_mouse_down(MouseButton::Left, |_, cx| { - cx.stop_propagation() - }) - .on_click(cx.listener_for(&self.editor, { - move |editor, _, cx| { - editor.jump( - jump_data.path.clone(), - jump_data.position, - jump_data.anchor, - jump_data.line_offset_from_top, - cx, - ); - } - })) - }), - ) - .children(show_excerpt_controls.then(|| { - h_flex() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.333))) - .h(1. * cx.line_height()) - .pt_1() - .justify_end() + .w(icon_offset) + .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height()) .flex_none() - .w(icon_offset - header_padding) - .child( - ButtonLike::new("expand-icon") - .style(ButtonStyle::Transparent) - .child( - svg() - .path(IconName::ArrowUpFromLine.path()) - .size(IconSize::XSmall.rems()) - .text_color(cx.theme().colors().editor_line_number) - .group("") - .hover(|style| { - style.text_color( - cx.theme() - .colors() - .editor_active_line_number, - ) - }), - ) - .on_click(cx.listener_for(&self.editor, { - let id = *id; - move |editor, _, cx| { - editor.expand_excerpt( - id, - multi_buffer::ExpandExcerptDirection::Up, - cx, - ); - } - })) - .tooltip({ - move |cx| { - Tooltip::for_action( - "Expand Excerpt", - &ExpandExcerpts { lines: 0 }, - cx, - ) - } - }), - ) - })) - } else { - v_flex() - .id(("excerpt header", EntityId::from(block_id))) - .w_full() - .h(snapshot.excerpt_header_height() as f32 * cx.line_height()) - .child( + .justify_end() + .child(self.render_expand_excerpt_button( + prev_excerpt.id, + ExpandExcerptDirection::Down, + IconName::ArrowDownFromLine, + cx, + )), + ); + } + } + + if let Some(next_excerpt) = next_excerpt { + let buffer = &next_excerpt.buffer; + let range = &next_excerpt.range; + let jump_data = project::File::from_dyn(buffer.file()).map(|file| { + let jump_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }; + let jump_anchor = range + .primary + .as_ref() + .map_or(range.context.start, |primary| primary.start); + + let excerpt_start = range.context.start; + let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); + let offset_from_excerpt_start = if jump_anchor == excerpt_start { + 0 + } else { + let excerpt_start_row = + language::ToPoint::to_point(&jump_anchor, buffer).row; + jump_position.row - excerpt_start_row + }; + + let line_offset_from_top = + block_row_start.0 + *height + offset_from_excerpt_start + - snapshot + .scroll_anchor + .scroll_position(&snapshot.display_snapshot) + .y as u32; + + JumpData { + position: jump_position, + anchor: jump_anchor, + path: jump_path, + line_offset_from_top, + } + }); + + if *starts_new_buffer { + let include_root = self + .editor + .read(cx) + .project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(); + let path = buffer.resolve_file_path(cx, include_root); + let filename = path + .as_ref() + .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string())); + let parent_path = path.as_ref().and_then(|path| { + Some(path.parent()?.to_string_lossy().to_string() + "/") + }); + + result = result.child( div() - .flex() - .v_flex() + .px(header_padding) + .pt(header_padding) + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * cx.line_height()) + .child( + h_flex() + .id("path header block") + .size_full() + .flex_basis(Length::Definite(DefiniteLength::Fraction( + 0.667, + ))) + .px(gpui::px(12.)) + .rounded_md() + .shadow_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_subheader_background) + .justify_between() + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .child( + h_flex().gap_3().child( + h_flex() + .gap_2() + .child( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + ) + .when_some(parent_path, |then, path| { + then.child(div().child(path).text_color( + cx.theme().colors().text_muted, + )) + }), + ), + ) + .when_some(jump_data, |el, jump_data| { + el.child(Icon::new(IconName::ArrowUpRight)) + .cursor_pointer() + .tooltip(|cx| { + Tooltip::for_action( + "Jump to File", + &OpenExcerpts, + cx, + ) + }) + .on_mouse_down(MouseButton::Left, |_, cx| { + cx.stop_propagation() + }) + .on_click(cx.listener_for(&self.editor, { + move |editor, _, cx| { + editor.jump( + jump_data.path.clone(), + jump_data.position, + jump_data.anchor, + jump_data.line_offset_from_top, + cx, + ); + } + })) + }), + ), + ); + if *show_excerpt_controls { + result = result.child( + h_flex() + .w(icon_offset) + .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height()) + .flex_none() + .justify_end() + .child(self.render_expand_excerpt_button( + next_excerpt.id, + ExpandExcerptDirection::Up, + IconName::ArrowUpFromLine, + cx, + )), + ); + } + } else { + result = result.child( + h_flex() + .id("excerpt header block") + .group("excerpt-jump-action") .justify_start() - .id("jump to collapsed context") - .w(relative(1.0)) - .h_full() + .w_full() + .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height()) + .relative() .child( div() - .h_px() + .top(px(0.)) + .absolute() .w_full() + .h_px() .bg(cx.theme().colors().border_variant) .group_hover("excerpt-jump-action", |style| { style.bg(cx.theme().colors().border) }), - ), - ) - .child( - h_flex() - .justify_end() - .flex_none() - .w(icon_offset) - .h_full() + ) + .cursor_pointer() + .when_some(jump_data.clone(), |this, jump_data| { + this.on_click(cx.listener_for(&self.editor, { + let path = jump_data.path.clone(); + move |editor, _, cx| { + cx.stop_propagation(); + + editor.jump( + path.clone(), + jump_data.position, + jump_data.anchor, + jump_data.line_offset_from_top, + cx, + ); + } + })) + .tooltip(move |cx| { + Tooltip::for_action( + format!( + "Jump to {}:L{}", + jump_data.path.path.display(), + jump_data.position.row + 1 + ), + &OpenExcerpts, + cx, + ) + }) + }) .child( - show_excerpt_controls - .then(|| { - ButtonLike::new("expand-icon") - .style(ButtonStyle::Transparent) - .child( - svg() - .path(IconName::ArrowUpFromLine.path()) - .size(IconSize::XSmall.rems()) - .text_color( - cx.theme().colors().editor_line_number, - ) - .group("") - .hover(|style| { - style.text_color( - cx.theme() - .colors() - .editor_active_line_number, - ) - }), - ) - .on_click(cx.listener_for(&self.editor, { - let id = *id; - move |editor, _, cx| { - editor.expand_excerpt( - id, - multi_buffer::ExpandExcerptDirection::Up, - cx, - ); - } - })) - .tooltip({ - move |cx| { - Tooltip::for_action( - "Expand Excerpt", - &ExpandExcerpts { lines: 0 }, - cx, - ) - } - }) - }) - .unwrap_or_else(|| { + h_flex() + .w(icon_offset) + .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 + * cx.line_height()) + .flex_none() + .justify_end() + .child(if *show_excerpt_controls { + self.render_expand_excerpt_button( + next_excerpt.id, + ExpandExcerptDirection::Up, + IconName::ArrowUpFromLine, + cx, + ) + } else { ButtonLike::new("jump-icon") .style(ButtonStyle::Transparent) .child( @@ -2361,7 +2347,6 @@ impl EditorElement { .text_color( cx.theme().colors().border_variant, ) - .group("excerpt-jump-action") .group_hover( "excerpt-jump-action", |style| { @@ -2371,118 +2356,13 @@ impl EditorElement { }, ), ) - .when_some(jump_data.clone(), |this, jump_data| { - this.on_click(cx.listener_for(&self.editor, { - let path = jump_data.path.clone(); - move |editor, _, cx| { - cx.stop_propagation(); - - editor.jump( - path.clone(), - jump_data.position, - jump_data.anchor, - jump_data.line_offset_from_top, - cx, - ); - } - })) - .tooltip(move |cx| { - Tooltip::for_action( - format!( - "Jump to {}:L{}", - jump_data.path.path.display(), - jump_data.position.row + 1 - ), - &OpenExcerpts, - cx, - ) - }) - }) }), ), - ) - .group("excerpt-jump-action") - .cursor_pointer() - .when_some(jump_data.clone(), |this, jump_data| { - this.on_click(cx.listener_for(&self.editor, { - let path = jump_data.path.clone(); - move |editor, _, cx| { - cx.stop_propagation(); + ); + } + } - editor.jump( - path.clone(), - jump_data.position, - jump_data.anchor, - jump_data.line_offset_from_top, - cx, - ); - } - })) - .tooltip(move |cx| { - Tooltip::for_action( - format!( - "Jump to {}:L{}", - jump_data.path.path.display(), - jump_data.position.row + 1 - ), - &OpenExcerpts, - cx, - ) - }) - }) - }; - element.into_any() - } - - Block::ExcerptFooter { id, .. } => { - let element = v_flex() - .id(("excerpt footer", EntityId::from(block_id))) - .w_full() - .h(snapshot.excerpt_footer_height() as f32 * cx.line_height()) - .child( - h_flex() - .justify_end() - .flex_none() - .w(gutter_dimensions.width - - (gutter_dimensions.left_padding + gutter_dimensions.margin)) - .h_full() - .child( - ButtonLike::new("expand-icon") - .style(ButtonStyle::Transparent) - .child( - svg() - .path(IconName::ArrowDownFromLine.path()) - .size(IconSize::XSmall.rems()) - .text_color(cx.theme().colors().editor_line_number) - .group("") - .hover(|style| { - style.text_color( - cx.theme().colors().editor_active_line_number, - ) - }), - ) - .on_click(cx.listener_for(&self.editor, { - let id = *id; - move |editor, _, cx| { - editor.expand_excerpt( - id, - multi_buffer::ExpandExcerptDirection::Down, - cx, - ); - } - })) - .tooltip({ - move |cx| { - Tooltip::for_action( - "Expand Excerpt", - &ExpandExcerpts { lines: 0 }, - cx, - ) - } - }), - ), - ); - element.into_any() + result.into_any() } }; @@ -2509,6 +2389,33 @@ impl EditorElement { (element, final_size) } + fn render_expand_excerpt_button( + &self, + excerpt_id: ExcerptId, + direction: ExpandExcerptDirection, + icon: IconName, + cx: &mut WindowContext, + ) -> ButtonLike { + ButtonLike::new("expand-icon") + .style(ButtonStyle::Transparent) + .child( + svg() + .path(icon.path()) + .size(IconSize::XSmall.rems()) + .text_color(cx.theme().colors().editor_line_number) + .group("") + .hover(|style| style.text_color(cx.theme().colors().editor_active_line_number)), + ) + .on_click(cx.listener_for(&self.editor, { + move |editor, _, cx| { + editor.expand_excerpt(excerpt_id, direction, cx); + } + })) + .tooltip({ + move |cx| Tooltip::for_action("Expand Excerpt", &ExpandExcerpts { lines: 0 }, cx) + }) + } + #[allow(clippy::too_many_arguments)] fn render_blocks( &self, @@ -3367,7 +3274,7 @@ impl EditorElement { let end_row_in_current_excerpt = snapshot .blocks_in_range(start_row..end_row) .find_map(|(start_row, block)| { - if matches!(block, Block::ExcerptHeader { .. }) { + if matches!(block, Block::ExcerptBoundary { .. }) { Some(start_row) } else { None diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 19e2a4ea95..19ba147e16 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -952,7 +952,7 @@ mod tests { px(14.0), None, true, - 2, + 0, 2, 0, FoldPlaceholder::test(), diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index a239b5b770..f091c86ed9 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -189,6 +189,7 @@ pub struct MultiBufferSnapshot { show_headers: bool, } +#[derive(Clone)] pub struct ExcerptInfo { pub id: ExcerptId, pub buffer: BufferSnapshot, @@ -201,6 +202,7 @@ impl std::fmt::Debug for ExcerptInfo { f.debug_struct(type_name::()) .field("id", &self.id) .field("buffer_id", &self.buffer_id) + .field("path", &self.buffer.file().map(|f| f.path())) .field("range", &self.range) .finish() } diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index fcfc717efb..7f312023c3 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -17,8 +17,7 @@ use editor::{ use futures::io::BufReader; use futures::{AsyncBufReadExt as _, FutureExt as _, StreamExt as _}; use gpui::{ - div, prelude::*, EntityId, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, - WeakView, + div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView, }; use language::Point; use project::Fs; @@ -149,23 +148,21 @@ impl EditorBlock { .w(text_line_height) .h(text_line_height) .child( - IconButton::new( - ("close_output_area", EntityId::from(cx.block_id)), - IconName::Close, - ) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .size(ButtonSize::Compact) - .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::text("Close output area", cx)) - .on_click(move |_, cx| { - if let BlockId::Custom(block_id) = block_id { - (on_close)(block_id, cx) - } - }), + IconButton::new("close_output_area", IconName::Close) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .shape(IconButtonShape::Square) + .tooltip(|cx| Tooltip::text("Close output area", cx)) + .on_click(move |_, cx| { + if let BlockId::Custom(block_id) = block_id { + (on_close)(block_id, cx) + } + }), ); div() + .id(cx.block_id) .flex() .items_start() .min_h(text_line_height)