Add a command to expand the context for a multibuffer (#10300)
This PR adds an action to expand the excerpts lines of context in a multibuffer. Release Notes: - Added an `editor::ExpandExcerpts` action (bound to `shift-enter` by default), which can expand the excerpt the cursor is currently in by 3 lines. You can customize the number of lines by rebinding this action like so: ```json5 // In your keybindings array... { "context": "Editor && mode == full", "bindings": { "shift-enter": ["editor::ExpandExcerpts", { "lines": 5 }], } } ``` --------- Co-authored-by: Nathan <nathan@zed.dev> Co-authored-by: Max <max@zed.dev>
This commit is contained in:
parent
9d9bce08a7
commit
8a02159b82
14 changed files with 353 additions and 45 deletions
|
@ -24,14 +24,18 @@ test-support = [
|
|||
anyhow.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rand.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
|
|
|
@ -7,6 +7,7 @@ use collections::{BTreeMap, Bound, HashMap, HashSet};
|
|||
use futures::{channel::mpsc, SinkExt};
|
||||
use git::diff::DiffHunk;
|
||||
use gpui::{AppContext, EventEmitter, Model, ModelContext};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
char_kind,
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
|
@ -15,6 +16,7 @@ use language::{
|
|||
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
|
||||
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Ref, RefCell},
|
||||
|
@ -1008,12 +1010,12 @@ impl MultiBuffer {
|
|||
anchor_ranges.extend(ranges.by_ref().take(range_count).map(|range| {
|
||||
let start = Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id: excerpt_id,
|
||||
excerpt_id,
|
||||
text_anchor: buffer_snapshot.anchor_after(range.start),
|
||||
};
|
||||
let end = Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id: excerpt_id,
|
||||
excerpt_id,
|
||||
text_anchor: buffer_snapshot.anchor_after(range.end),
|
||||
};
|
||||
start..end
|
||||
|
@ -1573,6 +1575,86 @@ impl MultiBuffer {
|
|||
self.as_singleton().unwrap().read(cx).is_parsing()
|
||||
}
|
||||
|
||||
pub fn expand_excerpts(
|
||||
&mut self,
|
||||
ids: impl IntoIterator<Item = ExcerptId>,
|
||||
line_count: u32,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if line_count == 0 {
|
||||
return;
|
||||
}
|
||||
self.sync(cx);
|
||||
|
||||
let snapshot = self.snapshot(cx);
|
||||
let locators = snapshot.excerpt_locators_for_ids(ids);
|
||||
let mut new_excerpts = SumTree::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
|
||||
let mut edits = Vec::<Edit<usize>>::new();
|
||||
|
||||
for locator in &locators {
|
||||
let prefix = cursor.slice(&Some(locator), Bias::Left, &());
|
||||
new_excerpts.append(prefix, &());
|
||||
|
||||
let mut excerpt = cursor.item().unwrap().clone();
|
||||
let old_text_len = excerpt.text_summary.len;
|
||||
|
||||
let start_row = excerpt
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.to_point(&excerpt.buffer)
|
||||
.row
|
||||
.saturating_sub(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),
|
||||
Bias::Left,
|
||||
);
|
||||
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);
|
||||
|
||||
let new_start_offset = new_excerpts.summary().text.len;
|
||||
let old_start_offset = cursor.start().1;
|
||||
let edit = Edit {
|
||||
old: old_start_offset..old_start_offset + old_text_len,
|
||||
new: new_start_offset..new_start_offset + excerpt.text_summary.len,
|
||||
};
|
||||
|
||||
if let Some(last_edit) = edits.last_mut() {
|
||||
if last_edit.old.end == edit.old.start {
|
||||
last_edit.old.end = edit.old.end;
|
||||
last_edit.new.end = edit.new.end;
|
||||
} else {
|
||||
edits.push(edit);
|
||||
}
|
||||
} else {
|
||||
edits.push(edit);
|
||||
}
|
||||
|
||||
new_excerpts.push(excerpt, &());
|
||||
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
new_excerpts.append(cursor.suffix(&()), &());
|
||||
|
||||
drop(cursor);
|
||||
self.snapshot.borrow_mut().excerpts = new_excerpts;
|
||||
|
||||
self.subscriptions.publish_mut(edits);
|
||||
cx.emit(Event::Edited {
|
||||
singleton_buffer_edited: false,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn sync(&self, cx: &AppContext) {
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
let mut excerpts_to_edit = Vec::new();
|
||||
|
@ -1796,6 +1878,19 @@ impl MultiBuffer {
|
|||
log::info!("Clearing multi-buffer");
|
||||
self.clear(cx);
|
||||
continue;
|
||||
} else if rng.gen_bool(0.1) && !self.excerpt_ids().is_empty() {
|
||||
let ids = self.excerpt_ids();
|
||||
let mut excerpts = HashSet::default();
|
||||
for _ in 0..rng.gen_range(0..ids.len()) {
|
||||
excerpts.extend(ids.choose(rng).copied());
|
||||
}
|
||||
|
||||
let line_count = rng.gen_range(0..5);
|
||||
|
||||
log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
|
||||
|
||||
self.expand_excerpts(excerpts.iter().cloned(), line_count, cx);
|
||||
continue;
|
||||
}
|
||||
|
||||
let excerpt_ids = self.excerpt_ids();
|
||||
|
@ -3361,6 +3456,39 @@ impl MultiBufferSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns the locators referenced by the given excerpt ids, sorted by locator.
|
||||
fn excerpt_locators_for_ids(
|
||||
&self,
|
||||
ids: impl IntoIterator<Item = ExcerptId>,
|
||||
) -> SmallVec<[Locator; 1]> {
|
||||
let mut sorted_ids = ids.into_iter().collect::<SmallVec<[_; 1]>>();
|
||||
sorted_ids.sort_unstable();
|
||||
let mut locators = SmallVec::new();
|
||||
|
||||
while sorted_ids.last() == Some(&ExcerptId::max()) {
|
||||
sorted_ids.pop();
|
||||
locators.push(Locator::max());
|
||||
}
|
||||
|
||||
let mut sorted_ids = sorted_ids.into_iter().dedup().peekable();
|
||||
if sorted_ids.peek() == Some(&ExcerptId::min()) {
|
||||
sorted_ids.next();
|
||||
locators.push(Locator::min());
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpt_ids.cursor::<ExcerptId>();
|
||||
for id in sorted_ids {
|
||||
if cursor.seek_forward(&id, Bias::Left, &()) {
|
||||
locators.push(cursor.item().unwrap().locator.clone());
|
||||
} else {
|
||||
panic!("invalid excerpt id {:?}", id);
|
||||
}
|
||||
}
|
||||
|
||||
locators.sort_unstable();
|
||||
locators
|
||||
}
|
||||
|
||||
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<BufferId> {
|
||||
Some(self.excerpt(excerpt_id)?.buffer_id)
|
||||
}
|
||||
|
@ -4286,7 +4414,8 @@ where
|
|||
.peekable();
|
||||
while let Some(range) = range_iter.next() {
|
||||
let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0);
|
||||
let mut excerpt_end = Point::new(range.end.row + context_line_count, 0).min(max_point);
|
||||
// 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 mut ranges_in_excerpt = 1;
|
||||
|
||||
|
@ -4323,6 +4452,13 @@ mod tests {
|
|||
use std::env;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_singleton(cx: &mut AppContext) {
|
||||
let buffer = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
|
||||
|
@ -4721,6 +4857,59 @@ mod tests {
|
|||
assert_eq!(*follower_edit_event_count.read(), 4);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_expand_excerpts(cx: &mut AppContext) {
|
||||
let buffer = cx.new_model(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts_with_context_lines(
|
||||
buffer.clone(),
|
||||
vec![
|
||||
// Note that in this test, this first excerpt
|
||||
// does not contain a new line
|
||||
Point::new(3, 2)..Point::new(3, 3),
|
||||
Point::new(7, 1)..Point::new(7, 3),
|
||||
Point::new(15, 0)..Point::new(15, 0),
|
||||
],
|
||||
1,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.expand_excerpts(multibuffer.excerpt_ids(), 1, cx)
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
|
||||
// Expanding context lines causes the line containing 'fff' to appear in two different excerpts.
|
||||
// We don't attempt to merge them, because removing the excerpt could create inconsistency with other layers
|
||||
// that are tracking excerpt ids.
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
concat!(
|
||||
"bbb\n", // Preserve newlines
|
||||
"ccc\n", //
|
||||
"ddd\n", //
|
||||
"eee\n", //
|
||||
"fff\n", // <- Same as below
|
||||
"\n", // Excerpt boundary
|
||||
"fff\n", // <- Same as above
|
||||
"ggg\n", //
|
||||
"hhh\n", //
|
||||
"iii\n", //
|
||||
"jjj\n", //
|
||||
"\n", //
|
||||
"nnn\n", //
|
||||
"ooo\n", //
|
||||
"ppp\n", //
|
||||
"qqq\n", //
|
||||
"rrr\n", //
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_push_excerpts_with_context_lines(cx: &mut AppContext) {
|
||||
let buffer = cx.new_model(|cx| Buffer::local(sample_text(20, 3, 'a'), cx));
|
||||
|
@ -4729,6 +4918,8 @@ mod tests {
|
|||
multibuffer.push_excerpts_with_context_lines(
|
||||
buffer.clone(),
|
||||
vec![
|
||||
// Note that in this test, this first excerpt
|
||||
// does contain a new line
|
||||
Point::new(3, 2)..Point::new(4, 2),
|
||||
Point::new(7, 1)..Point::new(7, 3),
|
||||
Point::new(15, 0)..Point::new(15, 0),
|
||||
|
@ -4741,7 +4932,23 @@ mod tests {
|
|||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
"bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\n"
|
||||
concat!(
|
||||
"bbb\n", // Preserve newlines
|
||||
"ccc\n", //
|
||||
"ddd\n", //
|
||||
"eee\n", //
|
||||
"fff\n", //
|
||||
"ggg\n", //
|
||||
"hhh\n", //
|
||||
"iii\n", //
|
||||
"jjj\n", //
|
||||
"\n", //
|
||||
"nnn\n", //
|
||||
"ooo\n", //
|
||||
"ppp\n", //
|
||||
"qqq\n", //
|
||||
"rrr\n", //
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
@ -4777,7 +4984,23 @@ mod tests {
|
|||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
"bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\n"
|
||||
concat!(
|
||||
"bbb\n", //
|
||||
"ccc\n", //
|
||||
"ddd\n", //
|
||||
"eee\n", //
|
||||
"fff\n", //
|
||||
"ggg\n", //
|
||||
"hhh\n", //
|
||||
"iii\n", //
|
||||
"jjj\n", //
|
||||
"\n", //
|
||||
"nnn\n", //
|
||||
"ooo\n", //
|
||||
"ppp\n", //
|
||||
"qqq\n", //
|
||||
"rrr\n", //
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
@ -5027,10 +5250,45 @@ mod tests {
|
|||
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
0..=19 if !buffers.is_empty() => {
|
||||
0..=14 if !buffers.is_empty() => {
|
||||
let buffer = buffers.choose(&mut rng).unwrap();
|
||||
buffer.update(cx, |buf, cx| buf.randomly_edit(&mut rng, 5, cx));
|
||||
}
|
||||
15..=19 if !expected_excerpts.is_empty() => {
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
let ids = multibuffer.excerpt_ids();
|
||||
let mut excerpts = HashSet::default();
|
||||
for _ in 0..rng.gen_range(0..ids.len()) {
|
||||
excerpts.extend(ids.choose(&mut rng).copied());
|
||||
}
|
||||
|
||||
let line_count = rng.gen_range(0..5);
|
||||
|
||||
let excerpt_ixs = excerpts
|
||||
.iter()
|
||||
.map(|id| excerpt_ids.iter().position(|i| i == id).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines");
|
||||
multibuffer.expand_excerpts(excerpts.iter().cloned(), line_count, cx);
|
||||
|
||||
if line_count > 0 {
|
||||
for id in excerpts {
|
||||
let excerpt_ix = excerpt_ids.iter().position(|&i| i == id).unwrap();
|
||||
let (buffer, range) = &mut expected_excerpts[excerpt_ix];
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let mut point_range = range.to_point(&snapshot);
|
||||
point_range.start =
|
||||
Point::new(point_range.start.row.saturating_sub(line_count), 0);
|
||||
point_range.end = snapshot.clip_point(
|
||||
Point::new(point_range.end.row + line_count, 0),
|
||||
Bias::Left,
|
||||
);
|
||||
*range = snapshot.anchor_before(point_range.start)
|
||||
..snapshot.anchor_after(point_range.end);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
20..=29 if !expected_excerpts.is_empty() => {
|
||||
let mut ids_to_remove = vec![];
|
||||
for _ in 0..rng.gen_range(1..=3) {
|
||||
|
@ -5093,8 +5351,9 @@ mod tests {
|
|||
_ => {
|
||||
let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
|
||||
let base_text = util::RandomCharIter::new(&mut rng)
|
||||
.take(10)
|
||||
.take(25)
|
||||
.collect::<String>();
|
||||
|
||||
buffers.push(cx.new_model(|cx| Buffer::local(base_text, cx)));
|
||||
buffers.last().unwrap()
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue