diff --git a/assets/icons/arrow_down_from_line.svg b/assets/icons/arrow_down_from_line.svg new file mode 100644 index 0000000000..89316973a0 --- /dev/null +++ b/assets/icons/arrow_down_from_line.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrow_up_from_line.svg b/assets/icons/arrow_up_from_line.svg new file mode 100644 index 0000000000..50a075e42b --- /dev/null +++ b/assets/icons/arrow_up_from_line.svg @@ -0,0 +1 @@ + diff --git a/assets/settings/default.json b/assets/settings/default.json index a1cc720e8c..09202a3679 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -124,6 +124,8 @@ "wrap_guides": [], // Hide the values of in variables from visual display in private files "redact_private_values": false, + // The default number of lines to expand excerpts in the multibuffer by. + "expand_excerpt_lines": 3, // Globs to match against file paths to determine if a file is private. "private_files": [ "**/.env*", diff --git a/crates/assistant2/src/tools/annotate_code.rs b/crates/assistant2/src/tools/annotate_code.rs index afee701054..fc9e84351a 100644 --- a/crates/assistant2/src/tools/annotate_code.rs +++ b/crates/assistant2/src/tools/annotate_code.rs @@ -253,7 +253,7 @@ impl ToolView for AnnotationResultView { MultiBuffer::new(0, language::Capability::ReadWrite).with_title(String::new()) }); let editor = cx.new_view(|cx| { - Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), cx) + Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), true, cx) }); self.editor = Some(editor.clone()); diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index e5b314d9fe..ca61ad75ea 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -237,8 +237,9 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext = MarkdownPreviewView::new( MarkdownPreviewMode::Default, diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index e6904edfb1..5cf7e82b3f 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -308,8 +308,9 @@ async fn test_basic_following( result }); let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { - let editor = - cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); + let editor = cx.new_view(|cx| { + Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx) + }); workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx); editor }); diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 314ec0ac08..cc5da74cb1 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -781,7 +781,7 @@ mod tests { ); multibuffer }); - let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx)); + let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx)); editor.update(cx, |editor, cx| editor.focus(cx)).unwrap(); let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); editor @@ -811,7 +811,7 @@ mod tests { assert!(editor.has_active_inline_completion(cx)); assert_eq!( editor.display_text(cx), - "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" + "\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n" ); assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); }); @@ -833,7 +833,7 @@ mod tests { assert!(!editor.has_active_inline_completion(cx)); assert_eq!( editor.display_text(cx), - "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" + "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n" ); assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); @@ -842,7 +842,7 @@ mod tests { assert!(!editor.has_active_inline_completion(cx)); assert_eq!( editor.display_text(cx), - "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" + "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n" ); assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); }); @@ -853,7 +853,7 @@ mod tests { assert!(editor.has_active_inline_completion(cx)); assert_eq!( editor.display_text(cx), - "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" + "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n" ); assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); }); @@ -1032,7 +1032,7 @@ mod tests { ); multibuffer }); - let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx)); + let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx)); let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); editor .update(cx, |editor, cx| { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index f0357463c4..a590ed9d92 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -161,7 +161,7 @@ impl ProjectDiagnosticsEditor { }); let editor = cx.new_view(|cx| { let mut editor = - Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx); + Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx); editor.set_vertical_scroll_margin(5, cx); editor }); @@ -792,13 +792,15 @@ impl Item for ProjectDiagnosticsEditor { } } +const DIAGNOSTIC_HEADER: &'static str = "diagnostic header"; + fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let (message, code_ranges) = highlight_diagnostic_message(&diagnostic); let message: SharedString = message; Box::new(move |cx| { let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into(); h_flex() - .id("diagnostic header") + .id(DIAGNOSTIC_HEADER) .py_2() .pl_10() .pr_5() diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index f456020e84..04a1ce03f3 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -158,11 +158,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) { assert_eq!( editor_blocks(&editor, cx), [ - (DisplayRow(0), "path header block".into()), - (DisplayRow(2), "diagnostic header".into()), - (DisplayRow(15), "collapsed context".into()), - (DisplayRow(16), "diagnostic header".into()), - (DisplayRow(25), "collapsed context".into()), + (DisplayRow(0), FILE_HEADER.into()), + (DisplayRow(2), DIAGNOSTIC_HEADER.into()), + (DisplayRow(15), EXCERPT_HEADER.into()), + (DisplayRow(16), DIAGNOSTIC_HEADER.into()), + (DisplayRow(25), EXCERPT_HEADER.into()), ] ); assert_eq!( @@ -243,13 +243,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) { assert_eq!( editor_blocks(&editor, cx), [ - (DisplayRow(0), "path header block".into()), - (DisplayRow(2), "diagnostic header".into()), - (DisplayRow(7), "path header block".into()), - (DisplayRow(9), "diagnostic header".into()), - (DisplayRow(22), "collapsed context".into()), - (DisplayRow(23), "diagnostic header".into()), - (DisplayRow(32), "collapsed context".into()), + (DisplayRow(0), FILE_HEADER.into()), + (DisplayRow(2), DIAGNOSTIC_HEADER.into()), + (DisplayRow(7), FILE_HEADER.into()), + (DisplayRow(9), DIAGNOSTIC_HEADER.into()), + (DisplayRow(22), EXCERPT_HEADER.into()), + (DisplayRow(23), DIAGNOSTIC_HEADER.into()), + (DisplayRow(32), EXCERPT_HEADER.into()), ] ); @@ -355,15 +355,15 @@ async fn test_diagnostics(cx: &mut TestAppContext) { assert_eq!( editor_blocks(&editor, cx), [ - (DisplayRow(0), "path header block".into()), - (DisplayRow(2), "diagnostic header".into()), - (DisplayRow(7), "collapsed context".into()), - (DisplayRow(8), "diagnostic header".into()), - (DisplayRow(13), "path header block".into()), - (DisplayRow(15), "diagnostic header".into()), - (DisplayRow(28), "collapsed context".into()), - (DisplayRow(29), "diagnostic header".into()), - (DisplayRow(38), "collapsed context".into()), + (DisplayRow(0), FILE_HEADER.into()), + (DisplayRow(2), DIAGNOSTIC_HEADER.into()), + (DisplayRow(7), EXCERPT_HEADER.into()), + (DisplayRow(8), DIAGNOSTIC_HEADER.into()), + (DisplayRow(13), FILE_HEADER.into()), + (DisplayRow(15), DIAGNOSTIC_HEADER.into()), + (DisplayRow(28), EXCERPT_HEADER.into()), + (DisplayRow(29), DIAGNOSTIC_HEADER.into()), + (DisplayRow(38), EXCERPT_HEADER.into()), ] ); @@ -493,8 +493,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { assert_eq!( editor_blocks(&editor, cx), [ - (DisplayRow(0), "path header block".into()), - (DisplayRow(2), "diagnostic header".into()), + (DisplayRow(0), FILE_HEADER.into()), + (DisplayRow(2), DIAGNOSTIC_HEADER.into()), ] ); assert_eq!( @@ -539,10 +539,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { assert_eq!( editor_blocks(&editor, cx), [ - (DisplayRow(0), "path header block".into()), - (DisplayRow(2), "diagnostic header".into()), - (DisplayRow(6), "collapsed context".into()), - (DisplayRow(7), "diagnostic header".into()), + (DisplayRow(0), FILE_HEADER.into()), + (DisplayRow(2), DIAGNOSTIC_HEADER.into()), + (DisplayRow(6), EXCERPT_HEADER.into()), + (DisplayRow(7), DIAGNOSTIC_HEADER.into()), ] ); assert_eq!( @@ -605,10 +605,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { assert_eq!( editor_blocks(&editor, cx), [ - (DisplayRow(0), "path header block".into()), - (DisplayRow(2), "diagnostic header".into()), - (DisplayRow(7), "collapsed context".into()), - (DisplayRow(8), "diagnostic header".into()), + (DisplayRow(0), FILE_HEADER.into()), + (DisplayRow(2), DIAGNOSTIC_HEADER.into()), + (DisplayRow(7), EXCERPT_HEADER.into()), + (DisplayRow(8), DIAGNOSTIC_HEADER.into()), ] ); assert_eq!( @@ -661,10 +661,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { assert_eq!( editor_blocks(&editor, cx), [ - (DisplayRow(0), "path header block".into()), - (DisplayRow(2), "diagnostic header".into()), - (DisplayRow(7), "collapsed context".into()), - (DisplayRow(8), "diagnostic header".into()), + (DisplayRow(0), FILE_HEADER.into()), + (DisplayRow(2), DIAGNOSTIC_HEADER.into()), + (DisplayRow(7), EXCERPT_HEADER.into()), + (DisplayRow(8), DIAGNOSTIC_HEADER.into()), ] ); assert_eq!( @@ -958,6 +958,10 @@ fn random_diagnostic( } } +const FILE_HEADER: &'static str = "file header"; +const EXCERPT_HEADER: &'static str = "excerpt header"; +const EXCERPT_FOOTER: &'static str = "excerpt footer"; + fn editor_blocks( editor: &View, cx: &mut VisualTestContext, @@ -996,11 +1000,12 @@ fn editor_blocks( starts_new_buffer, .. } => { if *starts_new_buffer { - "path header block".into() + FILE_HEADER.into() } else { - "collapsed context".into() + EXCERPT_HEADER.into() } } + TransformBlock::ExcerptFooter { .. } => EXCERPT_FOOTER.into(), }; Some((row, name)) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 1c44ccc6f3..98c33473e1 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -114,12 +114,26 @@ pub struct ExpandExcerpts { pub(super) lines: u32, } +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct ExpandExcerptsUp { + #[serde(default)] + pub(super) lines: u32, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct ExpandExcerptsDown { + #[serde(default)] + pub(super) lines: u32, +} + impl_actions!( editor, [ ConfirmCodeAction, ConfirmCompletion, ExpandExcerpts, + ExpandExcerptsUp, + ExpandExcerptsDown, FoldAt, MoveDownByLines, MovePageDown, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 8f1355a5c7..3bf5061264 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -112,8 +112,10 @@ impl DisplayMap { font: Font, font_size: Pixels, wrap_width: Option, + show_excerpt_controls: bool, buffer_header_height: u8, excerpt_header_height: u8, + excerpt_footer_height: u8, fold_placeholder: FoldPlaceholder, cx: &mut ModelContext, ) -> Self { @@ -124,8 +126,15 @@ impl DisplayMap { let (fold_map, snapshot) = FoldMap::new(snapshot); let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx); - let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height); + let block_map = BlockMap::new( + snapshot, + show_excerpt_controls, + buffer_header_height, + excerpt_header_height, + excerpt_footer_height, + ); let flap_map = FlapMap::default(); + cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach(); DisplayMap { @@ -380,6 +389,10 @@ impl DisplayMap { pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool { self.wrap_map.read(cx).is_rewrapping() } + + pub fn show_excerpt_controls(&self) -> bool { + self.block_map.show_excerpt_controls() + } } #[derive(Debug, Default)] @@ -1098,8 +1111,10 @@ pub mod tests { font("Helvetica"), font_size, wrap_width, + true, buffer_start_excerpt_header_height, excerpt_header_height, + 0, FoldPlaceholder::test(), cx, ) @@ -1344,8 +1359,10 @@ pub mod tests { font("Helvetica"), font_size, wrap_width, + true, 1, 1, + 0, FoldPlaceholder::test(), cx, ) @@ -1453,8 +1470,10 @@ pub mod tests { font("Helvetica"), font_size, None, + true, 1, 1, + 0, FoldPlaceholder::test(), cx, ) @@ -1549,6 +1568,8 @@ pub mod tests { font("Helvetica"), font_size, None, + true, + 1, 1, 1, FoldPlaceholder::test(), @@ -1650,8 +1671,10 @@ pub mod tests { font("Courier"), font_size, Some(px(40.0)), + true, 1, 1, + 0, FoldPlaceholder::test(), cx, ) @@ -1732,6 +1755,8 @@ pub mod tests { font("Courier"), font_size, None, + true, + 1, 1, 1, FoldPlaceholder::test(), @@ -1856,8 +1881,10 @@ pub mod tests { font("Helvetica"), font_size, None, + true, 1, 1, + 0, FoldPlaceholder::test(), cx, ); @@ -1893,8 +1920,10 @@ pub mod tests { font("Helvetica"), font_size, None, + true, 1, 1, + 0, FoldPlaceholder::test(), cx, ) @@ -1968,8 +1997,10 @@ pub mod tests { font("Helvetica"), font_size, None, + true, 1, 1, + 0, FoldPlaceholder::test(), cx, ) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 5d534e6017..1cb370d303 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -12,7 +12,7 @@ use std::{ cell::RefCell, cmp::{self, Ordering}, fmt::Debug, - ops::{Deref, DerefMut, Range}, + ops::{Deref, DerefMut, Range, RangeBounds}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, @@ -31,8 +31,10 @@ pub struct BlockMap { wrap_snapshot: RefCell, blocks: Vec>, transforms: RefCell>, + show_excerpt_controls: bool, buffer_header_height: u8, excerpt_header_height: u8, + excerpt_footer_height: u8, } pub struct BlockMapWriter<'a>(&'a mut BlockMap); @@ -92,6 +94,7 @@ pub struct BlockContext<'a, 'b> { pub editor_style: &'b EditorStyle, } +/// Whether the block should be considered above or below the anchor line #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum BlockDisposition { Above, @@ -104,6 +107,17 @@ struct Transform { block: Option, } +pub(crate) enum BlockType { + Custom(BlockId), + Header, + Footer, +} + +pub(crate) trait BlockLike { + fn block_type(&self) -> BlockType; + fn disposition(&self) -> BlockDisposition; +} + #[allow(clippy::large_enum_variant)] #[derive(Clone)] pub enum TransformBlock { @@ -114,7 +128,27 @@ pub enum TransformBlock { range: ExcerptRange, height: u8, starts_new_buffer: bool, + show_excerpt_controls: bool, }, + ExcerptFooter { + id: ExcerptId, + disposition: BlockDisposition, + height: u8, + }, +} + +impl BlockLike for TransformBlock { + fn block_type(&self) -> BlockType { + match self { + TransformBlock::Custom(block) => BlockType::Custom(block.id), + TransformBlock::ExcerptHeader { .. } => BlockType::Header, + TransformBlock::ExcerptFooter { .. } => BlockType::Footer, + } + } + + fn disposition(&self) -> BlockDisposition { + self.disposition() + } } impl TransformBlock { @@ -122,6 +156,7 @@ impl TransformBlock { match self { TransformBlock::Custom(block) => block.disposition, TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above, + TransformBlock::ExcerptFooter { disposition, .. } => *disposition, } } @@ -129,6 +164,7 @@ impl TransformBlock { match self { TransformBlock::Custom(block) => block.height, TransformBlock::ExcerptHeader { height, .. } => *height, + TransformBlock::ExcerptFooter { height, .. } => *height, } } } @@ -137,9 +173,23 @@ impl Debug for TransformBlock { 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, .. } => f + Self::ExcerptHeader { + buffer, + starts_new_buffer, + id, + .. + } => f .debug_struct("ExcerptHeader") + .field("id", &id) .field("path", &buffer.file().map(|f| f.path())) + .field("starts_new_buffer", &starts_new_buffer) + .finish(), + TransformBlock::ExcerptFooter { + id, disposition, .. + } => f + .debug_struct("ExcerptFooter") + .field("id", &id) + .field("disposition", &disposition) .finish(), } } @@ -170,8 +220,10 @@ pub struct BlockBufferRows<'a> { impl BlockMap { pub fn new( wrap_snapshot: WrapSnapshot, + show_excerpt_controls: bool, buffer_header_height: u8, excerpt_header_height: u8, + excerpt_footer_height: u8, ) -> Self { let row_count = wrap_snapshot.max_point().row() + 1; let map = Self { @@ -179,8 +231,10 @@ impl BlockMap { blocks: Vec::new(), transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())), wrap_snapshot: RefCell::new(wrap_snapshot.clone()), + show_excerpt_controls, buffer_header_height, excerpt_header_height, + excerpt_footer_height, }; map.sync( &wrap_snapshot, @@ -364,49 +418,20 @@ impl BlockMap { (position.row(), TransformBlock::Custom(block.clone())) }), ); + if buffer.show_headers() { - blocks_in_edit.extend( - buffer - .excerpt_boundaries_in_range((start_bound, end_bound)) - .map(|excerpt_boundary| { - ( - wrap_snapshot - .make_wrap_point( - Point::new(excerpt_boundary.row.0, 0), - Bias::Left, - ) - .row(), - TransformBlock::ExcerptHeader { - id: excerpt_boundary.id, - buffer: excerpt_boundary.buffer, - range: excerpt_boundary.range, - height: if excerpt_boundary.starts_new_buffer { - self.buffer_header_height - } else { - self.excerpt_header_height - }, - starts_new_buffer: excerpt_boundary.starts_new_buffer, - }, - ) - }), - ); + blocks_in_edit.extend(BlockMap::header_blocks( + self.show_excerpt_controls, + self.excerpt_footer_height, + self.buffer_header_height, + self.excerpt_header_height, + buffer, + (start_bound, end_bound), + wrap_snapshot, + )); } - // Place excerpt headers above custom blocks on the same row. - blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| { - row_a.cmp(row_b).then_with(|| match (block_a, block_b) { - ( - TransformBlock::ExcerptHeader { .. }, - TransformBlock::ExcerptHeader { .. }, - ) => Ordering::Equal, - (TransformBlock::ExcerptHeader { .. }, _) => Ordering::Less, - (_, TransformBlock::ExcerptHeader { .. }) => Ordering::Greater, - (TransformBlock::Custom(block_a), TransformBlock::Custom(block_b)) => block_a - .disposition - .cmp(&block_b.disposition) - .then_with(|| block_a.id.cmp(&block_b.id)), - }) - }); + BlockMap::sort_blocks(&mut blocks_in_edit); // For each of these blocks, insert a new isomorphic transform preceding the block, // and then insert the block itself. @@ -449,6 +474,95 @@ impl BlockMap { } } } + + pub fn show_excerpt_controls(&self) -> bool { + self.show_excerpt_controls + } + + pub fn header_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>( + show_excerpt_controls: bool, + excerpt_footer_height: u8, + buffer_header_height: u8, + excerpt_header_height: u8, + buffer: &'b multi_buffer::MultiBufferSnapshot, + range: R, + wrap_snapshot: &'c WrapSnapshot, + ) -> impl Iterator + 'b + where + R: RangeBounds, + T: multi_buffer::ToOffset, + { + buffer + .excerpt_boundaries_in_range(range) + .flat_map(move |excerpt_boundary| { + let wrap_row = wrap_snapshot + .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left) + .row(); + + [ + show_excerpt_controls + .then(|| { + excerpt_boundary.prev.as_ref().map(|prev| { + ( + wrap_row, + TransformBlock::ExcerptFooter { + id: prev.id, + height: excerpt_footer_height, + disposition: if excerpt_boundary.next.is_some() { + BlockDisposition::Above + } else { + BlockDisposition::Below + }, + }, + ) + }) + }) + .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, + TransformBlock::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, + }, + ) + }), + ] + }) + .flatten() + } + + pub(crate) fn sort_blocks(blocks: &mut Vec<(u32, B)>) { + // Place excerpt headers and footers above custom blocks on the same row + blocks.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| { + row_a.cmp(row_b).then_with(|| { + block_a + .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::Custom(a_id), BlockType::Custom(b_id)) => a_id.cmp(&b_id), + }) + }) + }); + } } fn push_isomorphic(tree: &mut SumTree, rows: u32) { @@ -996,6 +1110,8 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) { #[cfg(test)] mod tests { + use std::env; + use super::*; use crate::display_map::inlay_map::InlayMap; use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; @@ -1003,7 +1119,6 @@ mod tests { use multi_buffer::MultiBuffer; use rand::prelude::*; use settings::SettingsStore; - use std::env; use util::RandomCharIter; #[gpui::test] @@ -1034,7 +1149,7 @@ mod tests { let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap()); let (wrap_map, wraps_snapshot) = cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx)); - let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); + let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1); let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); let block_ids = writer.insert(vec![ @@ -1206,7 +1321,7 @@ mod tests { let (_, wraps_snapshot) = cx.update(|cx| { WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx) }); - let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); + let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0); let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); writer.insert(vec![ @@ -1252,9 +1367,11 @@ mod tests { let font_size = px(14.0); let buffer_start_header_height = rng.gen_range(1..=5); let excerpt_header_height = rng.gen_range(1..=5); + let excerpt_footer_height = rng.gen_range(1..=5); log::info!("Wrap width: {:?}", wrap_width); log::info!("Excerpt Header Height: {:?}", excerpt_header_height); + log::info!("Excerpt Footer Height: {:?}", excerpt_footer_height); let buffer = if rng.gen() { let len = rng.gen_range(0..10); @@ -1273,8 +1390,10 @@ mod tests { .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx)); let mut block_map = BlockMap::new( wraps_snapshot, + true, buffer_start_header_height, excerpt_header_height, + excerpt_footer_height, ); let mut custom_blocks = Vec::new(); @@ -1410,24 +1529,23 @@ mod tests { }, ) })); - expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map( - |boundary| { - let position = - wraps_snapshot.make_wrap_point(Point::new(boundary.row.0, 0), Bias::Left); - ( - position.row(), - ExpectedBlock::ExcerptHeader { - height: if boundary.starts_new_buffer { - buffer_start_header_height - } else { - excerpt_header_height - }, - starts_new_buffer: boundary.starts_new_buffer, - }, - ) - }, - )); - expected_blocks.sort_unstable(); + + // Note that this needs to be synced with the related section in BlockMap::sync + expected_blocks.extend( + BlockMap::header_blocks( + true, + excerpt_footer_height, + buffer_start_header_height, + excerpt_header_height, + &buffer_snapshot, + 0.., + &wraps_snapshot, + ) + .map(|(row, block)| (row, block.into())), + ); + + BlockMap::sort_blocks(&mut expected_blocks); + let mut sorted_blocks_iter = expected_blocks.into_iter().peekable(); let input_buffer_rows = buffer_snapshot @@ -1593,12 +1711,16 @@ mod tests { } } - #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] + #[derive(Debug, Eq, PartialEq)] enum ExpectedBlock { ExcerptHeader { height: u8, starts_new_buffer: bool, }, + ExcerptFooter { + height: u8, + disposition: BlockDisposition, + }, Custom { disposition: BlockDisposition, id: BlockId, @@ -1606,11 +1728,26 @@ mod tests { }, } + impl BlockLike for ExpectedBlock { + fn block_type(&self) -> BlockType { + match self { + ExpectedBlock::Custom { id, .. } => BlockType::Custom(*id), + ExpectedBlock::ExcerptHeader { .. } => BlockType::Header, + ExpectedBlock::ExcerptFooter { .. } => BlockType::Footer, + } + } + + fn disposition(&self) -> BlockDisposition { + self.disposition() + } + } + impl ExpectedBlock { fn height(&self) -> u8 { match self { ExpectedBlock::ExcerptHeader { height, .. } => *height, ExpectedBlock::Custom { height, .. } => *height, + ExpectedBlock::ExcerptFooter { height, .. } => *height, } } @@ -1618,6 +1755,7 @@ mod tests { match self { ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above, ExpectedBlock::Custom { disposition, .. } => *disposition, + ExpectedBlock::ExcerptFooter { disposition, .. } => *disposition, } } } @@ -1638,6 +1776,14 @@ mod tests { height, starts_new_buffer, }, + TransformBlock::ExcerptFooter { + height, + disposition, + .. + } => ExpectedBlock::ExcerptFooter { + height, + disposition, + }, } } } @@ -1654,6 +1800,7 @@ mod tests { match self { TransformBlock::Custom(block) => Some(block), TransformBlock::ExcerptHeader { .. } => None, + TransformBlock::ExcerptFooter { .. } => None, } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d61332d667..1e9eac8a60 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -100,7 +100,7 @@ pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; -use multi_buffer::{MultiBufferPoint, MultiBufferRow, ToOffsetUtf16}; +use multi_buffer::{ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16}; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::project_settings::{GitGutterSetting, ProjectSettings}; @@ -1529,19 +1529,25 @@ impl Editor { pub fn single_line(cx: &mut ViewContext) -> Self { let buffer = cx.new_model(|cx| Buffer::local("", cx)); let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::SingleLine, buffer, None, cx) + Self::new(EditorMode::SingleLine, buffer, None, false, cx) } pub fn multi_line(cx: &mut ViewContext) -> Self { let buffer = cx.new_model(|cx| Buffer::local("", cx)); let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::Full, buffer, None, cx) + Self::new(EditorMode::Full, buffer, None, false, cx) } pub fn auto_height(max_lines: usize, cx: &mut ViewContext) -> Self { let buffer = cx.new_model(|cx| Buffer::local("", cx)); let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::AutoHeight { max_lines }, buffer, None, cx) + Self::new( + EditorMode::AutoHeight { max_lines }, + buffer, + None, + false, + cx, + ) } pub fn for_buffer( @@ -1550,19 +1556,27 @@ impl Editor { cx: &mut ViewContext, ) -> Self { let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::Full, buffer, project, cx) + Self::new(EditorMode::Full, buffer, project, false, cx) } pub fn for_multibuffer( buffer: Model, project: Option>, + show_excerpt_controls: bool, cx: &mut ViewContext, ) -> Self { - Self::new(EditorMode::Full, buffer, project, cx) + Self::new(EditorMode::Full, buffer, project, show_excerpt_controls, cx) } pub fn clone(&self, cx: &mut ViewContext) -> Self { - let mut clone = Self::new(self.mode, self.buffer.clone(), self.project.clone(), cx); + let show_excerpt_controls = self.display_map.read(cx).show_excerpt_controls(); + let mut clone = Self::new( + self.mode, + self.buffer.clone(), + self.project.clone(), + show_excerpt_controls, + cx, + ); self.display_map.update(cx, |display_map, cx| { let snapshot = display_map.snapshot(cx); clone.display_map.update(cx, |display_map, cx| { @@ -1579,6 +1593,7 @@ impl Editor { mode: EditorMode, buffer: Model, project: Option>, + show_excerpt_controls: bool, cx: &mut ViewContext, ) -> Self { let style = cx.text_style(); @@ -1615,12 +1630,16 @@ impl Editor { }), }; let display_map = cx.new_model(|cx| { + let file_header_size = if show_excerpt_controls { 3 } else { 2 }; + DisplayMap::new( buffer.clone(), style.font(), font_size, None, - 2, + show_excerpt_controls, + file_header_size, + 1, 1, fold_placeholder, cx, @@ -4287,7 +4306,7 @@ impl Editor { workspace.update(&mut cx, |workspace, cx| { let project = workspace.project().clone(); let editor = - cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); + cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, cx)); workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx); editor.update(cx, |editor, cx| { editor.highlight_background::( @@ -8127,9 +8146,34 @@ impl Editor { } pub fn expand_excerpts(&mut self, action: &ExpandExcerpts, cx: &mut ViewContext) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::UpAndDown, cx) + } + + pub fn expand_excerpts_down( + &mut self, + action: &ExpandExcerptsDown, + cx: &mut ViewContext, + ) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Down, cx) + } + + pub fn expand_excerpts_up(&mut self, action: &ExpandExcerptsUp, cx: &mut ViewContext) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Up, cx) + } + + pub fn expand_excerpts_for_direction( + &mut self, + lines: u32, + direction: ExpandExcerptDirection, + cx: &mut ViewContext, + ) { let selections = self.selections.disjoint_anchors(); - let lines = if action.lines == 0 { 3 } else { action.lines }; + let lines = if lines == 0 { + EditorSettings::get_global(cx).expand_excerpt_lines + } else { + lines + }; self.buffer.update(cx, |buffer, cx| { buffer.expand_excerpts( @@ -8138,14 +8182,22 @@ impl Editor { .map(|selection| selection.head().excerpt_id) .dedup(), lines, + direction, cx, ) }) } - pub fn expand_excerpt(&mut self, excerpt: ExcerptId, cx: &mut ViewContext) { - self.buffer - .update(cx, |buffer, cx| buffer.expand_excerpts([excerpt], 3, cx)) + pub fn expand_excerpt( + &mut self, + excerpt: ExcerptId, + direction: ExpandExcerptDirection, + cx: &mut ViewContext, + ) { + let lines = EditorSettings::get_global(cx).expand_excerpt_lines; + self.buffer.update(cx, |buffer, cx| { + buffer.expand_excerpts([excerpt], lines, direction, cx) + }) } fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { @@ -8792,7 +8844,7 @@ impl Editor { }); let editor = cx.new_view(|cx| { - Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), cx) + Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), true, cx) }); editor.update(cx, |editor, cx| { editor.highlight_background::( diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index a09d1f1e8d..4de22ee954 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -21,6 +21,7 @@ pub struct EditorSettings { pub seed_search_query_from_cursor: SeedQuerySetting, pub multi_cursor_modifier: MultiCursorModifier, pub redact_private_values: bool, + pub expand_excerpt_lines: u32, #[serde(default)] pub double_click_in_multibuffer: DoubleClickInMultibuffer, } @@ -182,6 +183,11 @@ pub struct EditorSettingsContent { /// Default: false pub redact_private_values: Option, + /// How many lines to expand the multibuffer excerpts by default + /// + /// Default: 3 + pub expand_excerpt_lines: Option, + /// What to do when multibuffer is double clicked in some of its excerpts /// (parts of singleton buffers). /// diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 42824fd81b..de1eb6bebb 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4292,10 +4292,10 @@ async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new_multibuffer( cx, [ - indoc! { + &indoc! { "aaa\n«bbb\nccc\n»ddd" }, - indoc! { + &indoc! { "aaa\n«bbb\nccc\n»ddd" }, ], @@ -6033,8 +6033,15 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) { ); multi_buffer }); - let multi_buffer_editor = - cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx)); + let multi_buffer_editor = cx.new_view(|cx| { + Editor::new( + EditorMode::Full, + multi_buffer, + Some(project.clone()), + true, + cx, + ) + }); multi_buffer_editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::Next), cx, |s| s.select_ranges(Some(1..2))); @@ -9430,8 +9437,15 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { let project = Project::test(fs, ["/a".as_ref()], cx).await; let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - let multi_buffer_editor = - cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx)); + let multi_buffer_editor = cx.new_view(|cx| { + Editor::new( + EditorMode::Full, + multi_buffer, + Some(project.clone()), + true, + cx, + ) + }); let multibuffer_item_id = workspace .update(cx, |workspace, cx| { assert!( @@ -10358,28 +10372,18 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) let project = Project::test(fs, ["/a".as_ref()], cx).await; let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - let multi_buffer_editor = - cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx)); + let multi_buffer_editor = cx.new_view(|cx| { + Editor::new( + EditorMode::Full, + multi_buffer, + Some(project.clone()), + true, + cx, + ) + }); cx.executor().run_until_parked(); let expected_all_hunks = vec![ - ( - "bbbb\n".to_string(), - DiffHunkStatus::Removed, - DisplayRow(3)..DisplayRow(3), - ), - ( - "nnnn\n".to_string(), - DiffHunkStatus::Modified, - DisplayRow(16)..DisplayRow(17), - ), - ( - "".to_string(), - DiffHunkStatus::Added, - DisplayRow(31)..DisplayRow(32), - ), - ]; - let expected_all_hunks_shifted = vec![ ( "bbbb\n".to_string(), DiffHunkStatus::Removed, @@ -10388,12 +10392,29 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) ( "nnnn\n".to_string(), DiffHunkStatus::Modified, - DisplayRow(18)..DisplayRow(19), + DisplayRow(21)..DisplayRow(22), ), ( "".to_string(), DiffHunkStatus::Added, - DisplayRow(33)..DisplayRow(34), + DisplayRow(41)..DisplayRow(42), + ), + ]; + let expected_all_hunks_shifted = vec![ + ( + "bbbb\n".to_string(), + DiffHunkStatus::Removed, + DisplayRow(5)..DisplayRow(5), + ), + ( + "nnnn\n".to_string(), + DiffHunkStatus::Modified, + DisplayRow(23)..DisplayRow(24), + ), + ( + "".to_string(), + DiffHunkStatus::Added, + DisplayRow(43)..DisplayRow(44), ), ]; @@ -10418,8 +10439,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) assert_eq!( expanded_hunks_background_highlights(editor, cx), vec![ - DisplayRow(18)..=DisplayRow(18), - DisplayRow(33)..=DisplayRow(33) + DisplayRow(23)..=DisplayRow(23), + DisplayRow(43)..=DisplayRow(43) ], ); assert_eq!(all_hunks, expected_all_hunks_shifted); @@ -10450,8 +10471,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) assert_eq!( expanded_hunks_background_highlights(editor, cx), vec![ - DisplayRow(18)..=DisplayRow(18), - DisplayRow(33)..=DisplayRow(33) + DisplayRow(23)..=DisplayRow(23), + DisplayRow(43)..=DisplayRow(43) ], ); assert_eq!(all_hunks, expected_all_hunks_shifted); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 19a7703b0a..ecdf3c7623 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -30,11 +30,11 @@ 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, - FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, - ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, - Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, ViewContext, WeakView, - WindowContext, + 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, + ViewContext, WeakView, WindowContext, }; use itertools::Itertools; use language::language_settings::{ @@ -278,6 +278,8 @@ impl EditorElement { register_action(view, cx, Editor::redo_selection); if !view.read(cx).is_singleton(cx) { register_action(view, cx, Editor::expand_excerpts); + register_action(view, cx, Editor::expand_excerpts_up); + register_action(view, cx, Editor::expand_excerpts_down); } register_action(view, cx, Editor::go_to_diagnostic); register_action(view, cx, Editor::go_to_prev_diagnostic); @@ -1893,6 +1895,7 @@ impl EditorElement { .partition::, _>(|(_, block)| match block { TransformBlock::ExcerptHeader { .. } => false, TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, + TransformBlock::ExcerptFooter { .. } => false, }); let render_block = |block: &TransformBlock, @@ -1933,6 +1936,7 @@ impl EditorElement { starts_new_buffer, height, id, + show_excerpt_controls, .. } => { let include_root = self @@ -1986,6 +1990,9 @@ impl EditorElement { } }); + 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; @@ -1998,15 +2005,16 @@ impl EditorElement { .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/")); } + let header_padding = px(6.0); + v_flex() - .id(("path header container", block_id)) + .id(("path excerpt header", block_id)) .size_full() - .justify_center() - .p(gpui::px(6.)) + .p(header_padding) .child( h_flex() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) .id("path header block") - .size_full() .pl(gpui::px(12.)) .pr(gpui::px(8.)) .rounded_md() @@ -2059,9 +2067,56 @@ impl EditorElement { })) }), ) + .children(show_excerpt_controls.then(|| { + h_flex() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.333))) + .pt_1() + .justify_end() + .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(("collapsed context", block_id)) + .id(("excerpt header", block_id)) .size_full() .child( div() @@ -2085,44 +2140,94 @@ impl EditorElement { h_flex() .justify_end() .flex_none() - .w( - gutter_dimensions.width - (gutter_dimensions.left_padding), // + gutter_dimensions.right_padding) - ) + .w(icon_offset) .h_full() .child( - ButtonLike::new("expand-icon") - .style(ButtonStyle::Transparent) - .child( - svg() - .path(IconName::ExpandVertical.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, + 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, ) - }), - ) - .on_click(cx.listener_for(&self.editor, { - let id = *id; - move |editor, _, cx| { - editor.expand_excerpt(id, cx); - } - })) - .tooltip({ - move |cx| { - Tooltip::for_action( - "Expand Excerpt", - &ExpandExcerpts { lines: 0 }, - cx, - ) - } - }), + .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(|| { + ButtonLike::new("jump-icon") + .style(ButtonStyle::Transparent) + .child( + svg() + .path(IconName::ArrowUpRight.path()) + .size(IconSize::XSmall.rems()) + .text_color( + cx.theme().colors().border_variant, + ) + .group("excerpt-jump-action") + .group_hover("excerpt-jump-action", |style| { + style.text_color( + cx.theme().colors().border + + ) + }) + ) + .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") @@ -2157,6 +2262,53 @@ impl EditorElement { }; element.into_any() } + + TransformBlock::ExcerptFooter { id, .. } => { + let element = v_flex().id(("excerpt footer", block_id)).size_full().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() + } }; let size = element.layout_as_root(available_space, cx); @@ -2184,6 +2336,7 @@ impl EditorElement { let style = match block { TransformBlock::Custom(block) => block.style(), TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, + TransformBlock::ExcerptFooter { .. } => BlockStyle::Sticky, }; let width = match style { BlockStyle::Sticky => hitbox.size.width, @@ -5413,7 +5566,7 @@ mod tests { init_test(cx, |_| {}); let window = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); - Editor::new(EditorMode::Full, buffer, None, cx) + Editor::new(EditorMode::Full, buffer, None, true, cx) }); let editor = window.root(cx).unwrap(); @@ -5491,7 +5644,7 @@ mod tests { let window = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); - Editor::new(EditorMode::Full, buffer, None, cx) + Editor::new(EditorMode::Full, buffer, None, true, cx) }); let cx = &mut VisualTestContext::from_window(*window, cx); let editor = window.root(cx).unwrap(); @@ -5556,21 +5709,26 @@ mod tests { // multi-buffer support // in DisplayPoint coordinates, this is what we're dealing with: // 0: [[file - // 1: header]] - // 2: aaaaaa - // 3: bbbbbb - // 4: cccccc - // 5: - // 6: ... - // 7: ffffff - // 8: gggggg - // 9: hhhhhh - // 10: - // 11: [[file - // 12: header]] - // 13: bbbbbb - // 14: cccccc - // 15: dddddd + // 1: header + // 2: section]] + // 3: aaaaaa + // 4: bbbbbb + // 5: cccccc + // 6: + // 7: [[footer]] + // 8: [[header]] + // 9: ffffff + // 10: gggggg + // 11: hhhhhh + // 12: + // 13: [[footer]] + // 14: [[file + // 15: header + // 16: section]] + // 17: bbbbbb + // 18: cccccc + // 19: dddddd + // 20: [[footer]] let window = cx.add_window(|cx| { let buffer = MultiBuffer::build_multi( [ @@ -5588,7 +5746,7 @@ mod tests { ], cx, ); - Editor::new(EditorMode::Full, buffer, None, cx) + Editor::new(EditorMode::Full, buffer, None, true, cx) }); let editor = window.root(cx).unwrap(); let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); @@ -5613,21 +5771,21 @@ mod tests { // and doesn't allow selection to bleed through assert_eq!( local_selections[0].range, - DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(6), 0) + DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0) ); assert_eq!( local_selections[0].head, - DisplayPoint::new(DisplayRow(5), 0) + DisplayPoint::new(DisplayRow(6), 0) ); // moves cursor on buffer boundary back two lines // and doesn't allow selection to bleed through assert_eq!( local_selections[1].range, - DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(11), 0) + DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0) ); assert_eq!( local_selections[1].head, - DisplayPoint::new(DisplayRow(10), 0) + DisplayPoint::new(DisplayRow(12), 0) ); } @@ -5637,7 +5795,7 @@ mod tests { let window = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("", cx); - Editor::new(EditorMode::Full, buffer, None, cx) + Editor::new(EditorMode::Full, buffer, None, true, cx) }); let cx = &mut VisualTestContext::from_window(*window, cx); let editor = window.root(cx).unwrap(); @@ -5835,7 +5993,7 @@ mod tests { ); let window = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&input_text, cx); - Editor::new(editor_mode, buffer, None, cx) + Editor::new(editor_mode, buffer, None, true, cx) }); let cx = &mut VisualTestContext::from_window(*window, cx); let editor = window.root(cx).unwrap(); diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index f9db17ac91..a22399b44b 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -572,7 +572,7 @@ fn editor_with_deleted_text( ); }); - let mut editor = Editor::for_multibuffer(multi_buffer, None, cx); + let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx); editor.soft_wrap_mode_override = Some(language::language_settings::SoftWrap::None); editor.show_wrap_guides = Some(false); editor.show_gutter = false; diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 8a934ecbd7..037cc2fc96 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2662,8 +2662,8 @@ pub mod tests { }); cx.executor().run_until_parked(); - let editor = - cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor = cx + .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx)); let editor_edited = Arc::new(AtomicBool::new(false)); let fake_server = fake_servers.next().await.unwrap(); @@ -2871,6 +2871,7 @@ pub mod tests { "main hint #5".to_string(), "other hint(edited) #0".to_string(), "other hint(edited) #1".to_string(), + "other hint(edited) #2".to_string(), ]; assert_eq!( expected_hints, @@ -2881,8 +2882,8 @@ pub mod tests { assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let current_cache_version = editor.inlay_hint_cache().version; - // We expect two new hints for the excerpts from `other.rs`: - let expected_version = last_scroll_update_version + 2; + // We expect three new hints for the excerpts from `other.rs`: + let expected_version = last_scroll_update_version + 3; assert_eq!( current_cache_version, expected_version, @@ -2970,8 +2971,8 @@ pub mod tests { assert!(!buffer_2_excerpts.is_empty()); cx.executor().run_until_parked(); - let editor = - cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor = cx + .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx)); let editor_edited = Arc::new(AtomicBool::new(false)); let fake_server = fake_servers.next().await.unwrap(); let closure_editor_edited = Arc::clone(&editor_edited); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2c10212ea9..268c62a054 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -137,7 +137,7 @@ impl FollowableItem for Editor { cx.new_view(|cx| { let mut editor = - Editor::for_multibuffer(multibuffer, Some(project.clone()), cx); + Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx); editor.remote_id = Some(remote_id); editor }) @@ -1162,23 +1162,26 @@ impl SearchableItem for Editor { } } else { for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) { - let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer); - ranges.extend( - query - .search(&excerpt.buffer, Some(excerpt_range.clone())) - .await - .into_iter() - .map(|range| { - let start = excerpt - .buffer - .anchor_after(excerpt_range.start + range.start); - let end = excerpt - .buffer - .anchor_before(excerpt_range.start + range.end); - buffer.anchor_in_excerpt(excerpt.id, start).unwrap() - ..buffer.anchor_in_excerpt(excerpt.id, end).unwrap() - }), - ); + if let Some(next_excerpt) = excerpt.next { + let excerpt_range = + next_excerpt.range.context.to_offset(&next_excerpt.buffer); + ranges.extend( + query + .search(&next_excerpt.buffer, Some(excerpt_range.clone())) + .await + .into_iter() + .map(|range| { + let start = next_excerpt + .buffer + .anchor_after(excerpt_range.start + range.start); + let end = next_excerpt + .buffer + .anchor_before(excerpt_range.start + range.end); + buffer.anchor_in_excerpt(next_excerpt.id, start).unwrap() + ..buffer.anchor_in_excerpt(next_excerpt.id, end).unwrap() + }), + ); + } } } ranges diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 510d9569aa..05c53f668c 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -695,12 +695,15 @@ mod tests { let font_size = px(14.0); let buffer = MultiBuffer::build_simple(input_text, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); + let display_map = cx.new_model(|cx| { DisplayMap::new( buffer, font, font_size, None, + true, + 1, 1, 1, FoldPlaceholder::test(), @@ -917,8 +920,10 @@ mod tests { font, px(14.0), None, + true, 2, 2, + 0, FoldPlaceholder::test(), cx, ) diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 32fd03a385..8fe8f48668 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -109,7 +109,9 @@ pub fn expand_macro_recursively( MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name) }); workspace.add_item_to_active_pane( - Box::new(cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))), + Box::new( + cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx)), + ), None, cx, ); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index f1bdb26f34..15cd9e0c35 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -39,6 +39,8 @@ pub fn marked_display_snapshot( font, font_size, None, + true, + 1, 1, 1, FoldPlaceholder::test(), @@ -74,7 +76,7 @@ pub fn assert_text_with_selections( #[allow(dead_code)] #[cfg(any(test, feature = "test-support"))] pub(crate) fn build_editor(buffer: Model, cx: &mut ViewContext) -> Editor { - Editor::new(EditorMode::Full, buffer, None, cx) + Editor::new(EditorMode::Full, buffer, None, true, cx) } pub(crate) fn build_editor_with_project( @@ -82,7 +84,7 @@ pub(crate) fn build_editor_with_project( buffer: Model, cx: &mut ViewContext, ) -> Editor { - Editor::new(EditorMode::Full, buffer, Some(project), cx) + Editor::new(EditorMode::Full, buffer, Some(project), true, cx) } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index c54892f0ac..302d012885 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -22,6 +22,7 @@ use std::{ Arc, }, }; + use ui::Context; use util::{ assert_set_eq, @@ -149,6 +150,10 @@ impl EditorTestContext { self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) } + pub fn display_text(&mut self) -> String { + self.update_editor(|editor, cx| editor.display_text(cx)) + } + pub fn buffer(&mut self, read: F) -> T where F: FnOnce(&Buffer, &AppContext) -> T, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 93e970033f..cd0d6fd7eb 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -18,6 +18,7 @@ use language::{ }; use smallvec::SmallVec; use std::{ + any::type_name, borrow::Cow, cell::{Ref, RefCell}, cmp, fmt, @@ -173,17 +174,40 @@ pub struct MultiBufferSnapshot { show_headers: bool, } -/// A boundary between [`Excerpt`]s in a [`MultiBuffer`] -pub struct ExcerptBoundary { +pub struct ExcerptInfo { pub id: ExcerptId, - pub row: MultiBufferRow, pub buffer: BufferSnapshot, + pub buffer_id: BufferId, pub range: ExcerptRange, - /// It's possible to have multiple excerpts in the same buffer, - /// and they are rendered together without a new File header. - /// - /// This flag indicates that the excerpt is the first one in the buffer. - pub starts_new_buffer: bool, +} + +impl std::fmt::Debug for ExcerptInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(type_name::()) + .field("id", &self.id) + .field("buffer_id", &self.buffer_id) + .field("range", &self.range) + .finish() + } +} + +/// A boundary between [`Excerpt`]s in a [`MultiBuffer`] +#[derive(Debug)] +pub struct ExcerptBoundary { + pub prev: Option, + pub next: Option, + /// The row in the `MultiBuffer` where the boundary is located + pub row: MultiBufferRow, +} + +impl ExcerptBoundary { + pub fn starts_new_buffer(&self) -> bool { + match (self.prev.as_ref(), self.next.as_ref()) { + (None, _) => true, + (Some(_), None) => false, + (Some(prev), Some(next)) => prev.buffer_id != next.buffer_id, + } + } } /// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`]. @@ -281,6 +305,30 @@ struct ExcerptBytes<'a> { reversed: bool, } +pub enum ExpandExcerptDirection { + Up, + Down, + UpAndDown, +} + +impl ExpandExcerptDirection { + pub fn should_expand_up(&self) -> bool { + match self { + ExpandExcerptDirection::Up => true, + ExpandExcerptDirection::Down => false, + ExpandExcerptDirection::UpAndDown => true, + } + } + + pub fn should_expand_down(&self) -> bool { + match self { + ExpandExcerptDirection::Up => false, + ExpandExcerptDirection::Down => true, + ExpandExcerptDirection::UpAndDown => true, + } + } +} + #[derive(Clone, Debug, PartialEq)] pub struct MultiBufferIndentGuide { pub multibuffer_row_range: Range, @@ -1610,6 +1658,7 @@ impl MultiBuffer { &mut self, ids: impl IntoIterator, line_count: u32, + direction: ExpandExcerptDirection, cx: &mut ModelContext, ) { if line_count == 0 { @@ -1630,26 +1679,40 @@ impl MultiBuffer { let mut excerpt = cursor.item().unwrap().clone(); let old_text_len = excerpt.text_summary.len; + let up_line_count = if direction.should_expand_up() { + line_count + } else { + 0 + }; + let start_row = excerpt .range .context .start .to_point(&excerpt.buffer) .row - .saturating_sub(line_count); + .saturating_sub(up_line_count); let start_point = Point::new(start_row, 0); excerpt.range.context.start = excerpt.buffer.anchor_before(start_point); - let end_point = excerpt.buffer.clip_point( - excerpt.range.context.end.to_point(&excerpt.buffer) + Point::new(line_count, 0), + let down_line_count = if direction.should_expand_down() { + line_count + } else { + 0 + }; + + let mut end_point = excerpt.buffer.clip_point( + excerpt.range.context.end.to_point(&excerpt.buffer) + + Point::new(down_line_count, 0), Bias::Left, ); + end_point.column = excerpt.buffer.line_len(end_point.row); excerpt.range.context.end = excerpt.buffer.anchor_after(end_point); excerpt.max_buffer_row = end_point.row; excerpt.text_summary = excerpt .buffer - .text_summary_for_range(start_point..end_point); + .text_summary_for_range(excerpt.range.context.clone()); let new_start_offset = new_excerpts.summary().text.len; let old_start_offset = cursor.start().1; @@ -1920,7 +1983,12 @@ impl MultiBuffer { log::info!("Expanding excerpts {excerpts:?} by {line_count} lines"); - self.expand_excerpts(excerpts.iter().cloned(), line_count, cx); + self.expand_excerpts( + excerpts.iter().cloned(), + line_count, + ExpandExcerptDirection::UpAndDown, + cx, + ); continue; } @@ -3018,24 +3086,37 @@ impl MultiBufferSnapshot { cursor.next(&()); } - let mut prev_buffer_id = cursor.prev_item().map(|excerpt| excerpt.buffer_id); + let mut visited_end = false; std::iter::from_fn(move || { if self.singleton { None } else if bounds.contains(&cursor.start().0) { - let excerpt = cursor.item()?; - let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id; - let boundary = ExcerptBoundary { + let next = cursor.item().map(|excerpt| ExcerptInfo { id: excerpt.id, - row: MultiBufferRow(cursor.start().1.row), buffer: excerpt.buffer.clone(), + buffer_id: excerpt.buffer_id, range: excerpt.range.clone(), - starts_new_buffer, - }; + }); + + if next.is_none() { + if visited_end { + return None; + } else { + visited_end = true; + } + } + + let prev = cursor.prev_item().map(|prev_excerpt| ExcerptInfo { + id: prev_excerpt.id, + buffer: prev_excerpt.buffer.clone(), + buffer_id: prev_excerpt.buffer_id, + range: prev_excerpt.range.clone(), + }); + let row = MultiBufferRow(cursor.start().1.row); - prev_buffer_id = Some(excerpt.buffer_id); cursor.next(&()); - Some(boundary) + + Some(ExcerptBoundary { row, prev, next }) } else { None } @@ -4537,15 +4618,16 @@ where .peekable(); while let Some(range) = range_iter.next() { let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0); - // These + 1s ensure that we select the whole next line - let mut excerpt_end = Point::new(range.end.row + 1 + context_line_count, 0).min(max_point); + let row = (range.end.row + context_line_count).min(max_point.row); + let mut excerpt_end = Point::new(row, buffer.line_len(row)); let mut ranges_in_excerpt = 1; while let Some(next_range) = range_iter.peek() { if next_range.start.row <= excerpt_end.row + context_line_count { - excerpt_end = - Point::new(next_range.end.row + 1 + context_line_count, 0).min(max_point); + let row = (next_range.end.row + context_line_count).min(max_point.row); + excerpt_end = Point::new(row, buffer.line_len(row)); + ranges_in_excerpt += 1; range_iter.next(); } else { @@ -4866,15 +4948,17 @@ mod tests { ) -> Vec<(MultiBufferRow, String, bool)> { snapshot .excerpt_boundaries_in_range(range) - .map(|boundary| { - ( - boundary.row, - boundary - .buffer - .text_for_range(boundary.range.context) - .collect::(), - boundary.starts_new_buffer, - ) + .filter_map(|boundary| { + let starts_new_buffer = boundary.starts_new_buffer(); + boundary.next.map(|next| { + ( + boundary.row, + next.buffer + .text_for_range(next.range.context) + .collect::(), + starts_new_buffer, + ) + }) }) .collect::>() } @@ -5006,8 +5090,33 @@ mod tests { ) }); + let snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!( + snapshot.text(), + concat!( + "ccc\n", // + "ddd\n", // + "eee", // + "\n", // End of excerpt + "ggg\n", // + "hhh\n", // + "iii", // + "\n", // End of excerpt + "ooo\n", // + "ppp\n", // + "qqq", // End of excerpt + ) + ); + drop(snapshot); + multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_excerpts(multibuffer.excerpt_ids(), 1, cx) + multibuffer.expand_excerpts( + multibuffer.excerpt_ids(), + 1, + ExpandExcerptDirection::UpAndDown, + cx, + ) }); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -5018,23 +5127,21 @@ mod tests { assert_eq!( snapshot.text(), concat!( - "bbb\n", // Preserve newlines + "bbb\n", // "ccc\n", // "ddd\n", // "eee\n", // - "fff\n", // <- Same as below - "\n", // Excerpt boundary - "fff\n", // <- Same as above + "fff\n", // End of excerpt + "fff\n", // "ggg\n", // "hhh\n", // "iii\n", // - "jjj\n", // - "\n", // + "jjj\n", // End of excerpt "nnn\n", // "ooo\n", // "ppp\n", // "qqq\n", // - "rrr\n", // + "rrr", // End of excerpt ) ); } @@ -5071,12 +5178,11 @@ mod tests { "hhh\n", // "iii\n", // "jjj\n", // - "\n", // "nnn\n", // "ooo\n", // "ppp\n", // "qqq\n", // - "rrr\n", // + "rrr", // ) ); @@ -5088,7 +5194,7 @@ mod tests { vec![ Point::new(2, 2)..Point::new(3, 2), Point::new(6, 1)..Point::new(6, 3), - Point::new(12, 0)..Point::new(12, 0) + Point::new(11, 0)..Point::new(11, 0) ] ); } @@ -5123,12 +5229,11 @@ mod tests { "hhh\n", // "iii\n", // "jjj\n", // - "\n", // "nnn\n", // "ooo\n", // "ppp\n", // "qqq\n", // - "rrr\n", // + "rrr", // ) ); @@ -5140,7 +5245,7 @@ mod tests { vec![ Point::new(2, 2)..Point::new(3, 2), Point::new(6, 1)..Point::new(6, 3), - Point::new(12, 0)..Point::new(12, 0) + Point::new(11, 0)..Point::new(11, 0) ] ); } @@ -5404,7 +5509,12 @@ mod tests { .map(|id| excerpt_ids.iter().position(|i| i == id).unwrap()) .collect::>(); log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines"); - multibuffer.expand_excerpts(excerpts.iter().cloned(), line_count, cx); + multibuffer.expand_excerpts( + excerpts.iter().cloned(), + line_count, + ExpandExcerptDirection::UpAndDown, + cx, + ); if line_count > 0 { for id in excerpts { @@ -5418,6 +5528,7 @@ mod tests { Point::new(point_range.end.row + line_count, 0), Bias::Left, ); + point_range.end.column = snapshot.line_len(point_range.end.row); *range = snapshot.anchor_before(point_range.start) ..snapshot.anchor_after(point_range.end); } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index da28e55cb2..2d6d50464c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -653,7 +653,7 @@ impl ProjectSearchView { editor }); let results_editor = cx.new_view(|cx| { - let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx); + let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), true, cx); editor.set_searchable(false); editor }); @@ -1722,7 +1722,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" + "\n\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\n\n\nconst TWO: usize = one::ONE + one::ONE;\n" ); let match_background_color = cx.theme().colors().search_match_background; assert_eq!( @@ -1731,15 +1731,15 @@ pub mod tests { .update(cx, |editor, cx| editor.all_text_background_highlights(cx)), &[ ( - DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35), + DisplayPoint::new(DisplayRow(3), 32)..DisplayPoint::new(DisplayRow(3), 35), match_background_color ), ( - DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40), + DisplayPoint::new(DisplayRow(3), 37)..DisplayPoint::new(DisplayRow(3), 40), match_background_color ), ( - DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9), + DisplayPoint::new(DisplayRow(8), 6)..DisplayPoint::new(DisplayRow(8), 9), match_background_color ) ] @@ -1749,7 +1749,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)] + [DisplayPoint::new(DisplayRow(3), 32)..DisplayPoint::new(DisplayRow(3), 35)] ); search_view.select_match(Direction::Next, cx); @@ -1762,7 +1762,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)] + [DisplayPoint::new(DisplayRow(3), 37)..DisplayPoint::new(DisplayRow(3), 40)] ); search_view.select_match(Direction::Next, cx); }) @@ -1775,7 +1775,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)] + [DisplayPoint::new(DisplayRow(8), 6)..DisplayPoint::new(DisplayRow(8), 9)] ); search_view.select_match(Direction::Next, cx); }) @@ -1788,7 +1788,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)] + [DisplayPoint::new(DisplayRow(3), 32)..DisplayPoint::new(DisplayRow(3), 35)] ); search_view.select_match(Direction::Prev, cx); }) @@ -1801,7 +1801,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)] + [DisplayPoint::new(DisplayRow(8), 6)..DisplayPoint::new(DisplayRow(8), 9)] ); search_view.select_match(Direction::Prev, cx); }) @@ -1814,7 +1814,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)] + [DisplayPoint::new(DisplayRow(3), 37)..DisplayPoint::new(DisplayRow(3), 40)] ); }) .unwrap(); @@ -1982,7 +1982,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "\n\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\n\n\nconst TWO: usize = one::ONE + one::ONE;\n", "Search view results should match the query" ); assert!( @@ -2021,7 +2021,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "\n\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\n\n\nconst TWO: usize = one::ONE + one::ONE;\n", "Results should be unchanged after search view 2nd open in a row" ); assert!( @@ -2213,7 +2213,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "\n\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\n\n\nconst TWO: usize = one::ONE + one::ONE;\n", "Search view results should match the query" ); assert!( @@ -2268,7 +2268,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "\n\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\n\n\nconst TWO: usize = one::ONE + one::ONE;\n", "Results of the first search view should not update too" ); assert!( @@ -2317,7 +2317,7 @@ pub mod tests { search_view_2 .results_editor .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst FOUR: usize = one::ONE + three::THREE;", + "\n\n\nconst FOUR: usize = one::ONE + three::THREE;\n", "New search view with the updated query should have new search results" ); assert!( @@ -2462,7 +2462,7 @@ pub mod tests { search_view .results_editor .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "\n\n\nconst ONE: usize = 1;\n\n\n\n\nconst TWO: usize = one::ONE + one::ONE;\n", "New search in directory should have a filter that matches a certain directory" ); }) diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 2f9eae8684..f73b1d1613 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -77,9 +77,11 @@ pub enum IconName { Ai, ArrowCircle, ArrowDown, + ArrowDownFromLine, ArrowLeft, ArrowRight, ArrowUp, + ArrowUpFromLine, ArrowUpRight, AtSign, AudioOff, @@ -193,6 +195,7 @@ impl IconName { IconName::Ai => "icons/ai.svg", IconName::ArrowCircle => "icons/arrow_circle.svg", IconName::ArrowDown => "icons/arrow_down.svg", + IconName::ArrowDownFromLine => "icons/arrow_down_from_line.svg", IconName::ArrowLeft => "icons/arrow_left.svg", IconName::ArrowRight => "icons/arrow_right.svg", IconName::ArrowUp => "icons/arrow_up.svg", @@ -301,6 +304,7 @@ impl IconName { IconName::XCircle => "icons/error.svg", IconName::ZedAssistant => "icons/zed_assistant.svg", IconName::ZedXCopilot => "icons/zed_x_copilot.svg", + IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg", } } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c245d3518a..f2eb0bccb1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -601,8 +601,9 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { let buffer = cx.new_model(|cx| { MultiBuffer::singleton(buffer, cx).with_title("Log".into()) }); - let editor = - cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx)); + let editor = cx.new_view(|cx| { + Editor::for_multibuffer(buffer, Some(project), true, cx) + }); editor.update(cx, |editor, cx| { let last_multi_buffer_offset = editor.buffer().read(cx).len(cx); @@ -831,7 +832,7 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext