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 | 577 ++++++++------------ crates/editor/src/movement.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 2 + crates/repl/src/session.rs | 29 +- 7 files changed, 397 insertions(+), 549 deletions(-) diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 75bfd6415cc7b329eee6c018f4620bab55a25b08..1daffffb4eabcfcca7957f7f214f6a6693f7e69b 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 52e0ca2486d25d5c2994fafe23e6c00265f5dd61..f4ee57408b1ca8f4a77d407ce36d7a84a840b93e 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 9d9cfde7b9c119546babc0c8b698063036aa56ba..ba3841b4e2202ebd140a4606e1e9db8c0e8f212c 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 a9dfe7e435df8edf18bb6465a6a3d861643c5049..77b78d059c955b14dc28733630161faef7021988 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( - 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(|| { + if let Some(prev_excerpt) = prev_excerpt { + if *show_excerpt_controls { + result = result.child( 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) + .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() + .px(header_padding) + .pt(header_padding) + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * cx.line_height()) .child( - ButtonLike::new("expand-icon") - .style(ButtonStyle::Transparent) + 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( - 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, + 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, + )) + }), + ), ) - .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, - ) - } + .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, + ); + } + })) }), - ) - })) - } else { - v_flex() - .id(("excerpt header", EntityId::from(block_id))) - .w_full() - .h(snapshot.excerpt_header_height() as f32 * cx.line_height()) - .child( - div() - .flex() - .v_flex() + ), + ); + 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 19e2a4ea95a67602948777664970e3d2276084ff..19ba147e164dfde8a2c992b060942c76e5122140 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 a239b5b77049e54365a6ebab20e7b44eba60b367..f091c86ed92abd29acbfb0a66ee62a9c57b741be 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 fcfc717efb31de8554e78aa9bc42dcc82cbad9d2..7f312023c34aae9d0acf00ed0a0b09f2d0161597 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)