Files
zed/crates/editor/src/split.rs
T

857 lines
32 KiB
Rust

use std::ops::Range;
use buffer_diff::BufferDiff;
use collections::HashMap;
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
use gpui::{
Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
};
use language::{Buffer, Capability};
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, PathKey};
use project::Project;
use rope::Point;
use text::{Bias, OffsetRangeExt as _};
use ui::{
App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
Styled as _, Window, div,
};
use workspace::{
ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
};
use crate::{Editor, EditorEvent};
struct SplitDiffFeatureFlag;
impl FeatureFlag for SplitDiffFeatureFlag {
const NAME: &'static str = "split-diff";
fn enabled_for_staff() -> bool {
true
}
}
#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
#[action(namespace = editor)]
struct SplitDiff;
#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
#[action(namespace = editor)]
struct UnsplitDiff;
pub struct SplittableEditor {
primary_multibuffer: Entity<MultiBuffer>,
primary_editor: Entity<Editor>,
secondary: Option<SecondaryEditor>,
panes: PaneGroup,
workspace: WeakEntity<Workspace>,
_subscriptions: Vec<Subscription>,
}
struct SecondaryEditor {
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
pane: Entity<Pane>,
has_latest_selection: bool,
primary_to_secondary: HashMap<ExcerptId, ExcerptId>,
secondary_to_primary: HashMap<ExcerptId, ExcerptId>,
_subscriptions: Vec<Subscription>,
}
impl SplittableEditor {
pub fn primary_editor(&self) -> &Entity<Editor> {
&self.primary_editor
}
pub fn last_selected_editor(&self) -> &Entity<Editor> {
if let Some(secondary) = &self.secondary
&& secondary.has_latest_selection
{
&secondary.editor
} else {
&self.primary_editor
}
}
pub fn new_unsplit(
primary_multibuffer: Entity<MultiBuffer>,
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let primary_editor = cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
primary_multibuffer.clone(),
Some(project.clone()),
window,
cx,
);
editor.set_expand_all_diff_hunks(cx);
editor
});
let pane = cx.new(|cx| {
let mut pane = Pane::new(
workspace.downgrade(),
project,
Default::default(),
None,
NoAction.boxed_clone(),
true,
window,
cx,
);
pane.set_should_display_tab_bar(|_, _| false);
pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
pane
});
let panes = PaneGroup::new(pane);
// TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
let subscriptions = vec![cx.subscribe(
&primary_editor,
|this, _, event: &EditorEvent, cx| match event {
EditorEvent::ExpandExcerptsRequested {
excerpt_ids,
lines,
direction,
} => {
this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
}
EditorEvent::SelectionsChanged { .. } => {
if let Some(secondary) = &mut this.secondary {
secondary.has_latest_selection = false;
}
cx.emit(event.clone());
}
_ => cx.emit(event.clone()),
},
)];
window.defer(cx, {
let workspace = workspace.downgrade();
let primary_editor = primary_editor.downgrade();
move |window, cx| {
workspace
.update(cx, |workspace, cx| {
primary_editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx);
})
})
.ok();
}
});
Self {
primary_editor,
primary_multibuffer,
secondary: None,
panes,
workspace: workspace.downgrade(),
_subscriptions: subscriptions,
}
}
fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
if !cx.has_flag::<SplitDiffFeatureFlag>() {
return;
}
if self.secondary.is_some() {
return;
}
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let project = workspace.read(cx).project().clone();
let secondary_multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
multibuffer.set_all_diff_hunks_expanded(cx);
multibuffer
});
let secondary_editor = cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
secondary_multibuffer.clone(),
Some(project.clone()),
window,
cx,
);
editor.number_deleted_lines = true;
editor.set_delegate_expand_excerpts(true);
editor
});
let secondary_pane = cx.new(|cx| {
let mut pane = Pane::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
Default::default(),
None,
NoAction.boxed_clone(),
true,
window,
cx,
);
pane.set_should_display_tab_bar(|_, _| false);
pane.add_item(
ItemHandle::boxed_clone(&secondary_editor),
false,
false,
None,
window,
cx,
);
pane
});
let subscriptions = vec![cx.subscribe(
&secondary_editor,
|this, _, event: &EditorEvent, cx| match event {
EditorEvent::ExpandExcerptsRequested {
excerpt_ids,
lines,
direction,
} => {
if let Some(secondary) = &this.secondary {
let primary_ids: Vec<_> = excerpt_ids
.iter()
.filter_map(|id| secondary.secondary_to_primary.get(id).copied())
.collect();
this.expand_excerpts(primary_ids.into_iter(), *lines, *direction, cx);
}
}
EditorEvent::SelectionsChanged { .. } => {
if let Some(secondary) = &mut this.secondary {
secondary.has_latest_selection = true;
}
cx.emit(event.clone());
}
_ => cx.emit(event.clone()),
},
)];
let mut secondary = SecondaryEditor {
editor: secondary_editor,
multibuffer: secondary_multibuffer,
pane: secondary_pane.clone(),
has_latest_selection: false,
primary_to_secondary: HashMap::default(),
secondary_to_primary: HashMap::default(),
_subscriptions: subscriptions,
};
self.primary_editor.update(cx, |editor, cx| {
editor.set_delegate_expand_excerpts(true);
editor.buffer().update(cx, |primary_multibuffer, cx| {
primary_multibuffer.set_show_deleted_hunks(false, cx);
let paths = primary_multibuffer.paths().cloned().collect::<Vec<_>>();
for path in paths {
let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
else {
continue;
};
let snapshot = primary_multibuffer.snapshot(cx);
let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
secondary.sync_path_excerpts(path.clone(), primary_multibuffer, diff, cx);
}
})
});
self.secondary = Some(secondary);
let primary_pane = self.panes.first_pane();
self.panes
.split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
.unwrap();
cx.notify();
}
fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
let Some(secondary) = self.secondary.take() else {
return;
};
self.panes.remove(&secondary.pane, cx).unwrap();
self.primary_editor.update(cx, |primary, cx| {
primary.set_delegate_expand_excerpts(false);
primary.buffer().update(cx, |buffer, cx| {
buffer.set_show_deleted_hunks(true, cx);
});
});
cx.notify();
}
pub fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.workspace = workspace.weak_handle();
self.primary_editor.update(cx, |primary_editor, cx| {
primary_editor.added_to_workspace(workspace, window, cx);
});
if let Some(secondary) = &self.secondary {
secondary.editor.update(cx, |secondary_editor, cx| {
secondary_editor.added_to_workspace(workspace, window, cx);
});
}
}
pub fn set_excerpts_for_path(
&mut self,
path: PathKey,
buffer: Entity<Buffer>,
ranges: impl IntoIterator<Item = Range<Point>> + Clone,
context_line_count: u32,
diff: Entity<BufferDiff>,
cx: &mut Context<Self>,
) -> (Vec<Range<Anchor>>, bool) {
self.primary_multibuffer
.update(cx, |primary_multibuffer, cx| {
let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
path.clone(),
buffer.clone(),
ranges,
context_line_count,
cx,
);
if !anchors.is_empty()
&& primary_multibuffer
.diff_for(buffer.read(cx).remote_id())
.is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
{
primary_multibuffer.add_diff(diff.clone(), cx);
}
if let Some(secondary) = &mut self.secondary {
secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
}
(anchors, added_a_new_excerpt)
})
}
fn expand_excerpts(
&mut self,
excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
lines: u32,
direction: ExpandExcerptDirection,
cx: &mut Context<Self>,
) {
let mut corresponding_paths = HashMap::default();
self.primary_multibuffer.update(cx, |multibuffer, cx| {
let snapshot = multibuffer.snapshot(cx);
if self.secondary.is_some() {
corresponding_paths = excerpt_ids
.clone()
.map(|excerpt_id| {
let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
(path, diff)
})
.collect::<HashMap<_, _>>();
}
multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
});
if let Some(secondary) = &mut self.secondary {
self.primary_multibuffer.update(cx, |multibuffer, cx| {
for (path, diff) in corresponding_paths {
secondary.sync_path_excerpts(path, multibuffer, diff, cx);
}
})
}
}
pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
self.primary_multibuffer.update(cx, |buffer, cx| {
buffer.remove_excerpts_for_path(path.clone(), cx)
});
if let Some(secondary) = &mut self.secondary {
secondary.remove_mappings_for_path(&path, cx);
secondary
.multibuffer
.update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
}
}
}
#[cfg(test)]
impl SplittableEditor {
fn check_invariants(&self, quiesced: bool, cx: &App) {
use buffer_diff::DiffHunkStatusKind;
use collections::HashSet;
use multi_buffer::MultiBufferOffset;
use multi_buffer::MultiBufferRow;
use multi_buffer::MultiBufferSnapshot;
fn format_diff(snapshot: &MultiBufferSnapshot) -> String {
let text = snapshot.text();
let row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
let boundary_rows = snapshot
.excerpt_boundaries_in_range(MultiBufferOffset(0)..)
.map(|b| b.row)
.collect::<HashSet<_>>();
text.split('\n')
.enumerate()
.zip(row_infos)
.map(|((ix, line), info)| {
let marker = match info.diff_status.map(|status| status.kind) {
Some(DiffHunkStatusKind::Added) => "+ ",
Some(DiffHunkStatusKind::Deleted) => "- ",
Some(DiffHunkStatusKind::Modified) => unreachable!(),
None => {
if !line.is_empty() {
" "
} else {
""
}
}
};
let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {
" ----------\n"
} else {
""
};
let expand = info
.expand_info
.map(|expand_info| match expand_info.direction {
ExpandExcerptDirection::Up => " [↑]",
ExpandExcerptDirection::Down => " [↓]",
ExpandExcerptDirection::UpAndDown => " [↕]",
})
.unwrap_or_default();
format!("{boundary_row}{marker}{line}{expand}")
})
.collect::<Vec<_>>()
.join("\n")
}
let Some(secondary) = &self.secondary else {
return;
};
log::info!(
"primary:\n\n{}",
format_diff(&self.primary_multibuffer.read(cx).snapshot(cx))
);
log::info!(
"secondary:\n\n{}",
format_diff(&secondary.multibuffer.read(cx).snapshot(cx))
);
let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
assert_eq!(primary_excerpts.len(), secondary_excerpts.len());
assert_eq!(
secondary.primary_to_secondary.len(),
primary_excerpts.len(),
"primary_to_secondary mapping count should match excerpt count"
);
assert_eq!(
secondary.secondary_to_primary.len(),
secondary_excerpts.len(),
"secondary_to_primary mapping count should match excerpt count"
);
for primary_id in &primary_excerpts {
assert!(
secondary.primary_to_secondary.contains_key(primary_id),
"primary excerpt {:?} should have a mapping to secondary",
primary_id
);
}
for secondary_id in &secondary_excerpts {
assert!(
secondary.secondary_to_primary.contains_key(secondary_id),
"secondary excerpt {:?} should have a mapping to primary",
secondary_id
);
}
for (primary_id, secondary_id) in &secondary.primary_to_secondary {
assert_eq!(
secondary.secondary_to_primary.get(secondary_id),
Some(primary_id),
"mappings should be bijective"
);
}
if quiesced {
let primary_snapshot = self.primary_multibuffer.read(cx).snapshot(cx);
let secondary_snapshot = secondary.multibuffer.read(cx).snapshot(cx);
let primary_diff_hunks = primary_snapshot
.diff_hunks()
.map(|hunk| hunk.diff_base_byte_range)
.collect::<Vec<_>>();
let secondary_diff_hunks = secondary_snapshot
.diff_hunks()
.map(|hunk| hunk.diff_base_byte_range)
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(primary_diff_hunks, secondary_diff_hunks);
// Filtering out empty lines is a bit of a hack, to work around a case where
// the base text has a trailing newline but the current text doesn't, or vice versa.
// In this case, we get the additional newline on one side, but that line is not
// marked as added/deleted by rowinfos.
let primary_unmodified_rows = primary_snapshot
.text()
.split("\n")
.zip(primary_snapshot.row_infos(MultiBufferRow(0)))
.filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
.map(|(line, _)| line.to_owned())
.collect::<Vec<_>>();
let secondary_unmodified_rows = secondary_snapshot
.text()
.split("\n")
.zip(secondary_snapshot.row_infos(MultiBufferRow(0)))
.filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
.map(|(line, _)| line.to_owned())
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(primary_unmodified_rows, secondary_unmodified_rows);
}
}
fn randomly_edit_excerpts(
&mut self,
rng: &mut impl rand::Rng,
mutation_count: usize,
cx: &mut Context<Self>,
) {
use collections::HashSet;
use rand::prelude::*;
use std::env;
use util::RandomCharIter;
let max_excerpts = env::var("MAX_EXCERPTS")
.map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
.unwrap_or(5);
for _ in 0..mutation_count {
let paths = self
.primary_multibuffer
.read(cx)
.paths()
.cloned()
.collect::<Vec<_>>();
let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
let mut excerpts = HashSet::default();
for _ in 0..rng.random_range(0..excerpt_ids.len()) {
excerpts.extend(excerpt_ids.choose(rng).copied());
}
let line_count = rng.random_range(0..5);
log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
self.expand_excerpts(
excerpts.iter().cloned(),
line_count,
ExpandExcerptDirection::UpAndDown,
cx,
);
continue;
}
if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
let len = rng.random_range(100..500);
let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
let buffer = cx.new(|cx| Buffer::local(text, cx));
log::info!(
"Creating new buffer {} with text: {:?}",
buffer.read(cx).remote_id(),
buffer.read(cx).text()
);
let buffer_snapshot = buffer.read(cx).snapshot();
let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
// Create some initial diff hunks.
buffer.update(cx, |buffer, cx| {
buffer.randomly_edit(rng, 1, cx);
});
let buffer_snapshot = buffer.read(cx).text_snapshot();
let ranges = diff.update(cx, |diff, cx| {
diff.recalculate_diff_sync(&buffer_snapshot, cx);
diff.snapshot(cx)
.hunks(&buffer_snapshot)
.map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
.collect::<Vec<_>>()
});
let path = PathKey::for_buffer(&buffer, cx);
self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
} else {
let remove_count = rng.random_range(1..=paths.len());
let paths_to_remove = paths
.choose_multiple(rng, remove_count)
.cloned()
.collect::<Vec<_>>();
for path in paths_to_remove {
self.remove_excerpts_for_path(path.clone(), cx);
}
}
}
}
}
impl EventEmitter<EditorEvent> for SplittableEditor {}
impl Focusable for SplittableEditor {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.primary_editor.read(cx).focus_handle(cx)
}
}
impl Render for SplittableEditor {
fn render(
&mut self,
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let inner = if self.secondary.is_none() {
self.primary_editor.clone().into_any_element()
} else if let Some(active) = self.panes.panes().into_iter().next() {
self.panes
.render(
None,
&ActivePaneDecorator::new(active, &self.workspace),
window,
cx,
)
.into_any_element()
} else {
div().into_any_element()
};
div()
.id("splittable-editor")
.on_action(cx.listener(Self::split))
.on_action(cx.listener(Self::unsplit))
.size_full()
.child(inner)
}
}
impl SecondaryEditor {
fn sync_path_excerpts(
&mut self,
path_key: PathKey,
primary_multibuffer: &mut MultiBuffer,
diff: Entity<BufferDiff>,
cx: &mut App,
) {
let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path_key).next() else {
self.remove_mappings_for_path(&path_key, cx);
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.remove_excerpts_for_path(path_key, cx);
});
return;
};
let primary_excerpt_ids: Vec<ExcerptId> =
primary_multibuffer.excerpts_for_path(&path_key).collect();
let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
let main_buffer = primary_multibuffer_snapshot
.buffer_for_excerpt(excerpt_id)
.unwrap();
let base_text_buffer = diff.read(cx).base_text_buffer();
let diff_snapshot = diff.read(cx).snapshot(cx);
let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
let new = primary_multibuffer
.excerpts_for_buffer(main_buffer.remote_id(), cx)
.into_iter()
.map(|(_, excerpt_range)| {
let point_range_to_base_text_point_range = |range: Range<Point>| {
let start_row = diff_snapshot.row_to_base_text_row(
range.start.row,
Bias::Left,
main_buffer,
);
let end_row =
diff_snapshot.row_to_base_text_row(range.end.row, Bias::Right, main_buffer);
let end_column = diff_snapshot.base_text().line_len(end_row);
Point::new(start_row, 0)..Point::new(end_row, end_column)
};
let primary = excerpt_range.primary.to_point(main_buffer);
let context = excerpt_range.context.to_point(main_buffer);
ExcerptRange {
primary: point_range_to_base_text_point_range(primary),
context: point_range_to_base_text_point_range(context),
}
})
.collect();
let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
self.remove_mappings_for_path(&path_key, cx);
self.editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |buffer, cx| {
let (ids, _) = buffer.update_path_excerpts(
path_key.clone(),
base_text_buffer.clone(),
&base_text_buffer_snapshot,
new,
cx,
);
if !ids.is_empty()
&& buffer
.diff_for(base_text_buffer.read(cx).remote_id())
.is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
{
buffer.add_inverted_diff(diff, main_buffer, cx);
}
})
});
let secondary_excerpt_ids: Vec<ExcerptId> = self
.multibuffer
.read(cx)
.excerpts_for_path(&path_key)
.collect();
for (primary_id, secondary_id) in primary_excerpt_ids.into_iter().zip(secondary_excerpt_ids)
{
self.primary_to_secondary.insert(primary_id, secondary_id);
self.secondary_to_primary.insert(secondary_id, primary_id);
}
}
fn remove_mappings_for_path(&mut self, path_key: &PathKey, cx: &App) {
let secondary_excerpt_ids: Vec<ExcerptId> = self
.multibuffer
.read(cx)
.excerpts_for_path(path_key)
.collect();
for secondary_id in secondary_excerpt_ids {
if let Some(primary_id) = self.secondary_to_primary.remove(&secondary_id) {
self.primary_to_secondary.remove(&primary_id);
}
}
}
}
#[cfg(test)]
mod tests {
use fs::FakeFs;
use gpui::AppContext as _;
use language::Capability;
use multi_buffer::{MultiBuffer, PathKey};
use project::Project;
use rand::rngs::StdRng;
use settings::SettingsStore;
use ui::VisualContext as _;
use workspace::Workspace;
use crate::SplittableEditor;
fn init_test(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
crate::init(cx);
});
}
#[ignore]
#[gpui::test(iterations = 100)]
async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
use rand::prelude::*;
init_test(cx);
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let primary_multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
multibuffer.set_all_diff_hunks_expanded(cx);
multibuffer
});
let editor = cx.new_window_entity(|window, cx| {
let mut editor =
SplittableEditor::new_unsplit(primary_multibuffer, project, workspace, window, cx);
editor.split(&Default::default(), window, cx);
editor
});
let operations = std::env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(20);
let rng = &mut rng;
for _ in 0..operations {
editor.update(cx, |editor, cx| {
let buffers = editor
.primary_editor
.read(cx)
.buffer()
.read(cx)
.all_buffers();
if buffers.is_empty() {
editor.randomly_edit_excerpts(rng, 2, cx);
editor.check_invariants(true, cx);
return;
}
let quiesced = match rng.random_range(0..100) {
0..=69 if !buffers.is_empty() => {
let buffer = buffers.iter().choose(rng).unwrap();
buffer.update(cx, |buffer, cx| {
if rng.random() {
log::info!("randomly editing single buffer");
buffer.randomly_edit(rng, 5, cx);
} else {
log::info!("randomly undoing/redoing in single buffer");
buffer.randomly_undo_redo(rng, cx);
}
});
false
}
70..=79 => {
log::info!("mutating excerpts");
editor.randomly_edit_excerpts(rng, 2, cx);
false
}
80..=89 if !buffers.is_empty() => {
log::info!("recalculating buffer diff");
let buffer = buffers.iter().choose(rng).unwrap();
let diff = editor
.primary_multibuffer
.read(cx)
.diff_for(buffer.read(cx).remote_id())
.unwrap();
let buffer_snapshot = buffer.read(cx).text_snapshot();
diff.update(cx, |diff, cx| {
diff.recalculate_diff_sync(&buffer_snapshot, cx);
});
false
}
_ => {
log::info!("quiescing");
for buffer in buffers {
let buffer_snapshot = buffer.read(cx).text_snapshot();
let diff = editor
.primary_multibuffer
.read(cx)
.diff_for(buffer.read(cx).remote_id())
.unwrap();
diff.update(cx, |diff, cx| {
diff.recalculate_diff_sync(&buffer_snapshot, cx);
});
let diff_snapshot = diff.read(cx).snapshot(cx);
let ranges = diff_snapshot
.hunks(&buffer_snapshot)
.map(|hunk| hunk.range)
.collect::<Vec<_>>();
let path = PathKey::for_buffer(&buffer, cx);
editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
}
true
}
};
editor.check_invariants(quiesced, cx);
});
}
}
}