Handle a file's line endings changing on disk

This commit is contained in:
Max Brunsfeld 2022-07-04 12:28:55 -07:00
parent b0efa4f5c1
commit 0ba12eab22
2 changed files with 72 additions and 12 deletions

View file

@ -98,7 +98,7 @@ pub enum IndentKind {
Tab, Tab,
} }
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Debug, Clone, PartialEq, Eq)]
pub enum NewlineStyle { pub enum NewlineStyle {
Unix, Unix,
Windows, Windows,
@ -283,6 +283,7 @@ pub(crate) struct Diff {
base_version: clock::Global, base_version: clock::Global,
new_text: Arc<str>, new_text: Arc<str>,
changes: Vec<(ChangeTag, usize)>, changes: Vec<(ChangeTag, usize)>,
newline_style: NewlineStyle,
start_offset: usize, start_offset: usize,
} }
@ -973,6 +974,7 @@ impl Buffer {
let base_version = self.version(); let base_version = self.version();
cx.background().spawn(async move { cx.background().spawn(async move {
let old_text = old_text.to_string(); let old_text = old_text.to_string();
let newline_style = NewlineStyle::detect(&new_text);
let new_text = new_text.replace("\r\n", "\n").replace('\r', "\n"); let new_text = new_text.replace("\r\n", "\n").replace('\r', "\n");
let changes = TextDiff::from_lines(old_text.as_str(), new_text.as_str()) let changes = TextDiff::from_lines(old_text.as_str(), new_text.as_str())
.iter_all_changes() .iter_all_changes()
@ -982,6 +984,7 @@ impl Buffer {
base_version, base_version,
new_text: new_text.into(), new_text: new_text.into(),
changes, changes,
newline_style,
start_offset: 0, start_offset: 0,
} }
}) })
@ -995,6 +998,7 @@ impl Buffer {
if self.version == diff.base_version { if self.version == diff.base_version {
self.finalize_last_transaction(); self.finalize_last_transaction();
self.start_transaction(); self.start_transaction();
self.newline_style = diff.newline_style;
let mut offset = diff.start_offset; let mut offset = diff.start_offset;
for (tag, len) in diff.changes { for (tag, len) in diff.changes {
let range = offset..(offset + len); let range = offset..(offset + len);

View file

@ -2363,7 +2363,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
fs.remove_file("/dir/file2".as_ref(), Default::default()) fs.remove_file("/dir/file2".as_ref(), Default::default())
.await .await
.unwrap(); .unwrap();
buffer2.condition(&cx, |b, _| b.is_dirty()).await; cx.foreground().run_until_parked();
assert_eq!( assert_eq!(
*events.borrow(), *events.borrow(),
&[ &[
@ -2393,9 +2393,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
fs.remove_file("/dir/file3".as_ref(), Default::default()) fs.remove_file("/dir/file3".as_ref(), Default::default())
.await .await
.unwrap(); .unwrap();
buffer3 cx.foreground().run_until_parked();
.condition(&cx, |_, _| !events.borrow().is_empty())
.await;
assert_eq!(*events.borrow(), &[language::Event::FileHandleChanged]); assert_eq!(*events.borrow(), &[language::Event::FileHandleChanged]);
cx.read(|cx| assert!(buffer3.read(cx).is_dirty())); cx.read(|cx| assert!(buffer3.read(cx).is_dirty()));
} }
@ -2439,10 +2437,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
// Because the buffer was not modified, it is reloaded from disk. Its // Because the buffer was not modified, it is reloaded from disk. Its
// contents are edited according to the diff between the old and new // contents are edited according to the diff between the old and new
// file contents. // file contents.
buffer cx.foreground().run_until_parked();
.condition(&cx, |buffer, _| buffer.text() == new_contents)
.await;
buffer.update(cx, |buffer, _| { buffer.update(cx, |buffer, _| {
assert_eq!(buffer.text(), new_contents); assert_eq!(buffer.text(), new_contents);
assert!(!buffer.is_dirty()); assert!(!buffer.is_dirty());
@ -2476,9 +2471,70 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
// Because the buffer is modified, it doesn't reload from disk, but is // Because the buffer is modified, it doesn't reload from disk, but is
// marked as having a conflict. // marked as having a conflict.
buffer cx.foreground().run_until_parked();
.condition(&cx, |buffer, _| buffer.has_conflict()) buffer.read_with(cx, |buffer, _| {
.await; assert!(buffer.has_conflict());
});
}
#[gpui::test]
async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"file1": "a\nb\nc\n",
"file2": "one\r\ntwo\r\nthree\r\n",
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer1 = project
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await
.unwrap();
let buffer2 = project
.update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
.await
.unwrap();
buffer1.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "a\nb\nc\n");
assert_eq!(buffer.newline_style(), NewlineStyle::Unix);
});
buffer2.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "one\ntwo\nthree\n");
assert_eq!(buffer.newline_style(), NewlineStyle::Windows);
});
// Change a file's line endings on disk from unix to windows. The buffer's
// state updates correctly.
fs.save(
"/dir/file1".as_ref(),
&"aaa\nb\nc\n".into(),
NewlineStyle::Windows,
)
.await
.unwrap();
cx.foreground().run_until_parked();
buffer1.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "aaa\nb\nc\n");
assert_eq!(buffer.newline_style(), NewlineStyle::Windows);
});
// Save a file with windows line endings. The file is written correctly.
buffer2
.update(cx, |buffer, cx| {
buffer.set_text("one\ntwo\nthree\nfour\n", cx);
buffer.save(cx)
})
.await
.unwrap();
assert_eq!(
fs.load("/dir/file2".as_ref()).await.unwrap(),
"one\r\ntwo\r\nthree\r\nfour\r\n",
);
} }
#[gpui::test] #[gpui::test]