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