Compare commits
4 Commits
arm_github
...
cole/diff-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e1b077915 | ||
|
|
74b7e8ca32 | ||
|
|
ba2760544a | ||
|
|
20e24dca68 |
@@ -1096,136 +1096,290 @@ impl Render for ProjectDiffEditor {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures::future::join_all;
|
||||
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||
use project::buffer_store::BufferChangeSet;
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
prelude::*,
|
||||
};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
ops::Deref as _,
|
||||
ffi::OsString,
|
||||
ops::Not,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use text::{Edit, Patch, Rope};
|
||||
|
||||
use super::*;
|
||||
|
||||
// TODO finish
|
||||
// #[gpui::test]
|
||||
// async fn randomized_tests(cx: &mut TestAppContext) {
|
||||
// // Create a new project (how?? temp fs?),
|
||||
// let fs = FakeFs::new(cx.executor());
|
||||
// let project = Project::test(fs, [], cx).await;
|
||||
struct TestFile {
|
||||
name: OsString,
|
||||
staged_text: String,
|
||||
buffer: text::BufferSnapshot,
|
||||
editor: View<Editor>,
|
||||
patch: Patch<usize>,
|
||||
}
|
||||
|
||||
// // create random files with random content
|
||||
|
||||
// // Commit it into git somehow (technically can do with "real" fs in a temp dir)
|
||||
// //
|
||||
// // Apply randomized changes to the project: select a random file, random change and apply to buffers
|
||||
// }
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn simple_edit_test(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn random_edits(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
let rng = &mut rng;
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
".git": {},
|
||||
"file_a": "This is file_a",
|
||||
"file_b": "This is file_b",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [Path::new("/root")], cx).await;
|
||||
let names = ["file0", "file1", "file2", "file3", "file4"].map(str::to_owned);
|
||||
let fs = {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
let mut files = json!(names
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(std::iter::repeat_with(|| gen_file(rng).into()))
|
||||
.collect::<serde_json::Map<_, _>>());
|
||||
files
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(".git".to_owned(), json!({}));
|
||||
fs.insert_tree("/project", files).await;
|
||||
fs
|
||||
};
|
||||
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let file_a_editor = workspace
|
||||
let (file_editors, project_diff_editor) = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let file_a_editor =
|
||||
workspace.open_abs_path(PathBuf::from("/root/file_a"), true, cx);
|
||||
let file_editors = names.clone().map(|name| {
|
||||
workspace.open_abs_path(PathBuf::from(format!("/project/{}", name)), true, cx)
|
||||
});
|
||||
ProjectDiffEditor::deploy(workspace, &Deploy, cx);
|
||||
file_a_editor
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("did not open an item at all")
|
||||
.downcast::<Editor>()
|
||||
.expect("did not open an editor for file_a");
|
||||
let project_diff_editor = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
let project_diff_editor = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<ProjectDiffEditor>())
|
||||
.expect("Didn't open project diff editor");
|
||||
(file_editors, project_diff_editor)
|
||||
})
|
||||
.unwrap()
|
||||
.expect("did not find a ProjectDiffEditor");
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
assert!(
|
||||
project_diff_editor.editor.read(cx).text(cx).is_empty(),
|
||||
"Should have no changes after opening the diff on no git changes"
|
||||
);
|
||||
});
|
||||
|
||||
let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
|
||||
let change = "an edit after git add";
|
||||
file_a_editor
|
||||
.update(cx, |file_a_editor, cx| {
|
||||
file_a_editor.insert(change, cx);
|
||||
file_a_editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.unwrap();
|
||||
let file_editors = join_all(file_editors)
|
||||
.await
|
||||
.expect("failed to save a file");
|
||||
file_a_editor.update(cx, |file_a_editor, cx| {
|
||||
let change_set = cx.new_model(|cx| {
|
||||
BufferChangeSet::new_with_base_text(
|
||||
old_text.clone(),
|
||||
file_a_editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
file_a_editor
|
||||
.diff_map
|
||||
.add_change_set(change_set.clone(), cx);
|
||||
project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, cx| {
|
||||
buffer_store.set_change_set(
|
||||
file_a_editor
|
||||
.into_iter()
|
||||
.map(|result| {
|
||||
result
|
||||
.expect("Failed to open file editor")
|
||||
.downcast::<Editor>()
|
||||
.expect("Unexpected non-editor")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for file_editor in &file_editors {
|
||||
file_editor
|
||||
.update(cx, |editor, cx| editor.save(false, project.clone(), cx))
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
}
|
||||
let buffers = file_editors.clone().into_iter().map(|file_editor| {
|
||||
file_editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot()
|
||||
})
|
||||
});
|
||||
let mut files = names
|
||||
.into_iter()
|
||||
.zip(file_editors)
|
||||
.zip(buffers)
|
||||
.map(|((name, editor), buffer)| {
|
||||
let staged_text = buffer.text();
|
||||
TestFile {
|
||||
name: name.into(),
|
||||
editor,
|
||||
buffer,
|
||||
staged_text,
|
||||
patch: Patch::new(Vec::new()),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
check(project_diff_editor.clone(), &files, cx);
|
||||
for _ in 0..10 {
|
||||
let file = files.choose_mut(rng).unwrap();
|
||||
match rng.gen_range(0..5) {
|
||||
0..3 => {
|
||||
let old_text = file.buffer.as_rope().clone();
|
||||
let new_edits = gen_edits(rng, &old_text);
|
||||
file.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.edit(
|
||||
new_edits
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(old, _, content)| (old, content)),
|
||||
cx,
|
||||
);
|
||||
editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
let snapshot = file.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.remote_id(),
|
||||
change_set,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/.git"),
|
||||
&[(Path::new("file_a"), GitFileStatus::Modified)],
|
||||
);
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
.text_snapshot()
|
||||
});
|
||||
let diff = file
|
||||
.editor
|
||||
.update(cx, |editor, cx| {
|
||||
let change_set = cx.new_model(|cx| {
|
||||
BufferChangeSet::new_with_base_text(
|
||||
old_text.to_string(),
|
||||
snapshot.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
editor.diff_map.add_change_set_with_project(
|
||||
project.clone(),
|
||||
change_set,
|
||||
cx,
|
||||
);
|
||||
let diff = BufferDiff::build(&old_text, &snapshot);
|
||||
diff
|
||||
})
|
||||
.await;
|
||||
let patch = std::mem::take(&mut file.patch);
|
||||
file.patch = patch.compose(diff.hunks(&snapshot).map(|hunk| {
|
||||
let new = hunk.buffer_range.to_offset(&snapshot);
|
||||
let old = if hunk.diff_base_byte_range == (0..0) {
|
||||
new_edits
|
||||
.iter()
|
||||
.find_map(|(old, edit_new, _)| (edit_new == &new).then_some(old))
|
||||
.cloned()
|
||||
.unwrap()
|
||||
} else {
|
||||
hunk.diff_base_byte_range
|
||||
};
|
||||
Edit { old, new }
|
||||
}));
|
||||
file.buffer = snapshot;
|
||||
}
|
||||
3 => {
|
||||
file.staged_text = file.buffer.text();
|
||||
file.patch = Patch::new(Vec::new());
|
||||
}
|
||||
4 => {
|
||||
file.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.set_text(file.staged_text.as_str(), cx);
|
||||
editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
file.buffer = file.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot()
|
||||
});
|
||||
file.patch = Patch::new(Vec::new());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
assert_eq!(
|
||||
// TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
|
||||
project_diff_editor.editor.read(cx).text(cx),
|
||||
format!("{change}{old_text}"),
|
||||
"Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/project/.git"),
|
||||
&files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
file.patch
|
||||
.is_empty()
|
||||
.not()
|
||||
.then_some((file.name.as_ref(), GitFileStatus::Modified))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
check(project_diff_editor.clone(), &files, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn check(
|
||||
project_diff_editor: View<ProjectDiffEditor>,
|
||||
files: &[TestFile],
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
for file in files {
|
||||
assert_eq!(
|
||||
file.patch.is_empty(),
|
||||
file.staged_text == file.buffer.text(),
|
||||
)
|
||||
}
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
let snapshot = project_diff_editor
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
let hunks = snapshot
|
||||
.diff_map
|
||||
.diff_hunks(&snapshot.buffer_snapshot)
|
||||
.map(|hunk| {
|
||||
let buffer_snapshot = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_for_excerpt(hunk.excerpt_id)
|
||||
.unwrap();
|
||||
(
|
||||
hunk.buffer_id,
|
||||
hunk.diff_base_byte_range,
|
||||
hunk.buffer_range.to_offset(buffer_snapshot),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let edit_hunks = files
|
||||
.iter()
|
||||
.flat_map(|file| {
|
||||
file.patch.edits().iter().map(|Edit { old, new }| {
|
||||
(file.buffer.remote_id(), old.clone(), new.clone())
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(hunks.len(), edit_hunks.len());
|
||||
for edit_hunk in edit_hunks {
|
||||
assert!(hunks.iter().any(|hunk| hunk == &edit_hunk));
|
||||
}
|
||||
|
||||
let mut hunks = hunks.into_iter().peekable();
|
||||
for excerpt in snapshot.buffer_snapshot.all_excerpts() {
|
||||
let Some((buffer_id, _, new)) = hunks.next() else {
|
||||
panic!("Excerpt without a hunk")
|
||||
};
|
||||
let excerpt_buffer_id = excerpt.buffer().remote_id();
|
||||
let excerpt_range = excerpt.buffer_range().to_offset(excerpt.buffer());
|
||||
assert!(excerpt_buffer_id == buffer_id);
|
||||
assert!(excerpt_range.start <= new.start);
|
||||
assert!(excerpt_range.end >= new.end);
|
||||
while let Some((_, _, new)) = hunks.peek().map_or(None, |hunk @ (id, _, new)| {
|
||||
(id == &excerpt_buffer_id
|
||||
&& (excerpt_range.end > new.start
|
||||
|| (new.is_empty() && excerpt_range.end == new.start)))
|
||||
.then_some(hunk)
|
||||
}) {
|
||||
assert!(excerpt_range.start <= new.start);
|
||||
assert!(excerpt_range.end >= new.end);
|
||||
hunks.next();
|
||||
}
|
||||
}
|
||||
if let Some(hunk) = hunks.next() {
|
||||
panic!("Hunk without an excerpt: {hunk:?}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1248,4 +1402,71 @@ mod tests {
|
||||
cx.set_staff(true);
|
||||
});
|
||||
}
|
||||
|
||||
fn gen_line(rng: &mut StdRng) -> String {
|
||||
let len = rng.gen_range(0..20);
|
||||
let mut s = Alphanumeric.sample_string(rng, len);
|
||||
s.push('\n');
|
||||
s
|
||||
}
|
||||
|
||||
fn gen_file(rng: &mut StdRng) -> String {
|
||||
let line_count = rng.gen_range(0..10);
|
||||
(0..line_count).map(|_| gen_line(rng)).collect()
|
||||
}
|
||||
|
||||
fn gen_edits(rng: &mut StdRng, old: &Rope) -> Vec<(Range<usize>, Range<usize>, String)> {
|
||||
let mut old_lines = {
|
||||
let mut old_lines = Vec::new();
|
||||
let mut old_lines_iter = old.chunks().lines();
|
||||
while let Some(line) = old_lines_iter.next() {
|
||||
assert!(!line.ends_with("\n"));
|
||||
old_lines.push(line.to_owned());
|
||||
}
|
||||
if old_lines.last().is_some_and(|line| line.is_empty()) {
|
||||
old_lines.pop();
|
||||
}
|
||||
old_lines.into_iter()
|
||||
};
|
||||
let mut edits = Vec::new();
|
||||
let unchanged_count = rng.gen_range(0..=old_lines.len());
|
||||
let mut old_offset = old_lines
|
||||
.by_ref()
|
||||
.take(unchanged_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let mut new_offset = old_offset;
|
||||
while old_lines.len() > 0 {
|
||||
let deleted_count = rng.gen_range(0..=old_lines.len());
|
||||
let advance = old_lines
|
||||
.by_ref()
|
||||
.take(deleted_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let deleted_range = old_offset..old_offset + advance;
|
||||
old_offset += advance;
|
||||
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
|
||||
let added_count = rng.gen_range(minimum_added..=5);
|
||||
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
|
||||
let added_range = new_offset..new_offset + addition.len();
|
||||
new_offset += addition.len();
|
||||
edits.push((deleted_range, added_range, addition));
|
||||
|
||||
if old_lines.len() > 0 {
|
||||
let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
|
||||
if blank_lines == old_lines.len() {
|
||||
break;
|
||||
};
|
||||
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
let advance = old_lines
|
||||
.by_ref()
|
||||
.take(unchanged_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
old_offset += advance;
|
||||
new_offset += advance;
|
||||
}
|
||||
}
|
||||
edits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,40 @@ impl DiffMap {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn add_change_set_with_project(
|
||||
&mut self,
|
||||
project: Model<project::Project>,
|
||||
change_set: Model<BufferChangeSet>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let buffer_id = change_set.read(cx).buffer_id;
|
||||
self.snapshot
|
||||
.0
|
||||
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
|
||||
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
|
||||
self.diff_bases.insert(
|
||||
buffer_id,
|
||||
DiffBaseState {
|
||||
last_version: None,
|
||||
_subscription: cx.observe(&change_set, move |editor, change_set, cx| {
|
||||
editor
|
||||
.diff_map
|
||||
.snapshot
|
||||
.0
|
||||
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
|
||||
Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx);
|
||||
}),
|
||||
change_set: change_set.clone(),
|
||||
},
|
||||
);
|
||||
project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, _cx| {
|
||||
buffer_store.set_change_set(buffer_id, change_set);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_change_set(
|
||||
&mut self,
|
||||
change_set: Model<BufferChangeSet>,
|
||||
@@ -149,6 +183,7 @@ impl DiffMapSnapshot {
|
||||
let end =
|
||||
excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0));
|
||||
MultiBufferDiffHunk {
|
||||
excerpt_id: excerpt.id(),
|
||||
row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
|
||||
buffer_id,
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
@@ -185,6 +220,7 @@ impl DiffMapSnapshot {
|
||||
.map_point_from_buffer(Point::new(hunk.row_range.end, 0))
|
||||
.row;
|
||||
MultiBufferDiffHunk {
|
||||
excerpt_id: excerpt.id(),
|
||||
row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row),
|
||||
buffer_id,
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
@@ -1112,6 +1148,7 @@ pub(crate) fn to_diff_hunk(
|
||||
.multi_buffer_range
|
||||
.to_point(multi_buffer_snapshot);
|
||||
Some(MultiBufferDiffHunk {
|
||||
excerpt_id: hovered_hunk.multi_buffer_range.start.excerpt_id,
|
||||
row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
|
||||
buffer_id,
|
||||
buffer_range,
|
||||
|
||||
@@ -74,16 +74,23 @@ impl BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
|
||||
pub async fn build(diff_base: &Rope, buffer: &text::BufferSnapshot) -> Self {
|
||||
let mut tree = SumTree::new(buffer);
|
||||
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(diff_base, &buffer_text);
|
||||
let base_text = diff_base.to_string();
|
||||
let buffer_text = buffer.text();
|
||||
let patch = Self::diff(&base_text, &buffer_text);
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
let hunk = Self::process_patch_hunk(
|
||||
&patch,
|
||||
hunk_index,
|
||||
diff_base,
|
||||
buffer,
|
||||
&mut divergence,
|
||||
);
|
||||
tree.push(hunk, buffer);
|
||||
}
|
||||
}
|
||||
@@ -187,11 +194,11 @@ impl BufferDiff {
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
|
||||
*self = Self::build(&diff_base.to_string(), buffer).await;
|
||||
*self = Self::build(diff_base, buffer).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let start = text.anchor_before(Point::new(0, 0));
|
||||
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
|
||||
self.hunks_intersecting_range(start..end, text)
|
||||
@@ -222,6 +229,7 @@ impl BufferDiff {
|
||||
fn process_patch_hunk(
|
||||
patch: &GitPatch<'_>,
|
||||
hunk_index: usize,
|
||||
diff_base: &Rope,
|
||||
buffer: &text::BufferSnapshot,
|
||||
buffer_row_divergence: &mut i64,
|
||||
) -> InternalDiffHunk {
|
||||
@@ -231,51 +239,59 @@ impl BufferDiff {
|
||||
let mut first_deletion_buffer_row: Option<u32> = None;
|
||||
let mut buffer_row_range: Option<Range<u32>> = None;
|
||||
let mut diff_base_byte_range: Option<Range<usize>> = None;
|
||||
let mut first_addition_old_row: Option<u32> = None;
|
||||
|
||||
for line_index in 0..line_item_count {
|
||||
let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
|
||||
let kind = line.origin_value();
|
||||
let content_offset = line.content_offset() as isize;
|
||||
let content_len = line.content().len() as isize;
|
||||
match kind {
|
||||
GitDiffLineType::Addition => {
|
||||
if first_addition_old_row.is_none() {
|
||||
first_addition_old_row = Some(
|
||||
(line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
|
||||
);
|
||||
}
|
||||
*buffer_row_divergence += 1;
|
||||
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||
|
||||
if kind == GitDiffLineType::Addition {
|
||||
*buffer_row_divergence += 1;
|
||||
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||
|
||||
match &mut buffer_row_range {
|
||||
Some(buffer_row_range) => buffer_row_range.end = row + 1,
|
||||
None => buffer_row_range = Some(row..row + 1),
|
||||
match &mut buffer_row_range {
|
||||
Some(Range { end, .. }) => *end = row + 1,
|
||||
None => buffer_row_range = Some(row..row + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
GitDiffLineType::Deletion => {
|
||||
let end = content_offset + content_len;
|
||||
|
||||
if kind == GitDiffLineType::Deletion {
|
||||
let end = content_offset + content_len;
|
||||
match &mut diff_base_byte_range {
|
||||
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||
None => diff_base_byte_range = Some(content_offset as usize..end as usize),
|
||||
}
|
||||
|
||||
match &mut diff_base_byte_range {
|
||||
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||
None => diff_base_byte_range = Some(content_offset as usize..end as usize),
|
||||
if first_deletion_buffer_row.is_none() {
|
||||
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||
let row = old_row as i64 + *buffer_row_divergence;
|
||||
first_deletion_buffer_row = Some(row as u32);
|
||||
}
|
||||
|
||||
*buffer_row_divergence -= 1;
|
||||
}
|
||||
|
||||
if first_deletion_buffer_row.is_none() {
|
||||
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||
let row = old_row as i64 + *buffer_row_divergence;
|
||||
first_deletion_buffer_row = Some(row as u32);
|
||||
}
|
||||
|
||||
*buffer_row_divergence -= 1;
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
//unwrap_or deletion without addition
|
||||
let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
|
||||
//we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
|
||||
// Pure deletion hunk without addition.
|
||||
let row = first_deletion_buffer_row.unwrap();
|
||||
row..row
|
||||
});
|
||||
|
||||
//unwrap_or addition without deletion
|
||||
let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0);
|
||||
|
||||
let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
|
||||
// Pure addition hunk without deletion.
|
||||
let row = first_addition_old_row.unwrap();
|
||||
let offset = diff_base.point_to_offset(Point::new(row, 0));
|
||||
offset..offset
|
||||
});
|
||||
let start = Point::new(buffer_row_range.start, 0);
|
||||
let end = Point::new(buffer_row_range.end, 0);
|
||||
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||
|
||||
@@ -115,6 +115,8 @@ pub struct MultiBufferDiffHunk {
|
||||
pub row_range: Range<MultiBufferRow>,
|
||||
/// The buffer ID that this hunk belongs to.
|
||||
pub buffer_id: BufferId,
|
||||
/// The ID of the excerpt where this hunk appears.
|
||||
pub excerpt_id: ExcerptId,
|
||||
/// The range of the underlying buffer that this hunk corresponds to.
|
||||
pub buffer_range: Range<text::Anchor>,
|
||||
/// The range within the buffer's diff base that this hunk corresponds to.
|
||||
@@ -251,7 +253,7 @@ struct Excerpt {
|
||||
///
|
||||
/// Contains methods for getting the [`Buffer`] of the excerpt,
|
||||
/// as well as mapping offsets to/from buffer and multibuffer coordinates.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MultiBufferExcerpt<'a> {
|
||||
excerpt: &'a Excerpt,
|
||||
excerpt_offset: usize,
|
||||
@@ -3446,17 +3448,13 @@ impl MultiBufferSnapshot {
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<(usize, Point)>(&());
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
cursor.prev(&());
|
||||
|
||||
iter::from_fn(move || {
|
||||
let item = cursor.item()?;
|
||||
let hit =
|
||||
cursor.start().0 < range.end || (range.is_empty() && cursor.start().0 == range.end);
|
||||
let excerpt = hit.then(|| MultiBufferExcerpt::new(item, *cursor.start()));
|
||||
cursor.next(&());
|
||||
if cursor.start().0 < range.end {
|
||||
cursor
|
||||
.item()
|
||||
.map(|item| MultiBufferExcerpt::new(item, *cursor.start()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
excerpt
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2242,7 +2242,7 @@ impl BufferChangeSet {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> oneshot::Receiver<()> {
|
||||
LineEnding::normalize(&mut base_text);
|
||||
self.recalculate_diff_internal(base_text, buffer_snapshot, true, cx)
|
||||
self.recalculate_diff_internal(Rope::from(base_text), buffer_snapshot, true, cx)
|
||||
}
|
||||
|
||||
pub fn unset_base_text(
|
||||
@@ -2264,8 +2264,13 @@ impl BufferChangeSet {
|
||||
buffer_snapshot: text::BufferSnapshot,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> oneshot::Receiver<()> {
|
||||
if let Some(base_text) = self.base_text.clone() {
|
||||
self.recalculate_diff_internal(base_text.read(cx).text(), buffer_snapshot, false, cx)
|
||||
if let Some(base_text) = self.base_text.as_ref() {
|
||||
self.recalculate_diff_internal(
|
||||
base_text.read(cx).as_rope().clone(),
|
||||
buffer_snapshot,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
oneshot::channel().1
|
||||
}
|
||||
@@ -2273,7 +2278,7 @@ impl BufferChangeSet {
|
||||
|
||||
fn recalculate_diff_internal(
|
||||
&mut self,
|
||||
base_text: String,
|
||||
base_text: Rope,
|
||||
buffer_snapshot: text::BufferSnapshot,
|
||||
base_text_changed: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -2292,7 +2297,7 @@ impl BufferChangeSet {
|
||||
if base_text_changed {
|
||||
this.base_text_version += 1;
|
||||
this.base_text = Some(cx.new_model(|cx| {
|
||||
Buffer::local_normalized(Rope::from(base_text), LineEnding::default(), cx)
|
||||
Buffer::local_normalized(base_text, LineEnding::default(), cx)
|
||||
}));
|
||||
}
|
||||
this.diff_to_buffer = diff;
|
||||
|
||||
Reference in New Issue
Block a user