From 3480b50920e90f7da05ed997241af3a207238ca3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Jul 2022 17:25:19 +0200 Subject: [PATCH] Detect buffer newline style and honor it when saving --- crates/collab/src/integration_tests.rs | 14 +++- crates/language/src/buffer.rs | 92 ++++++++++++++++++++++---- crates/language/src/proto.rs | 2 +- crates/project/src/fs.rs | 22 ++++-- crates/project/src/project.rs | 13 ++-- crates/project/src/worktree.rs | 18 +++-- crates/rpc/proto/zed.proto | 6 ++ crates/zed/src/settings_file.rs | 2 + 8 files changed, 136 insertions(+), 33 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index ed429ea87c..9cefeac664 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -22,7 +22,7 @@ use gpui::{ }; use language::{ range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, - LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope, + LanguageConfig, LanguageRegistry, NewlineStyle, OffsetRangeExt, Point, Rope, }; use lsp::{self, FakeLanguageServer}; use parking_lot::Mutex; @@ -1263,7 +1263,11 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont client_a .fs - .save("/dir/a.txt".as_ref(), &"new contents".into()) + .save( + "/dir/a.txt".as_ref(), + &"new contents".into(), + NewlineStyle::Unix, + ) .await .unwrap(); buffer_b @@ -1857,7 +1861,11 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te client_a .fs - .save("/a/a.rs".as_ref(), &Rope::from("let seven = 7;")) + .save( + "/a/a.rs".as_ref(), + &Rope::from("let seven = 7;"), + NewlineStyle::Unix, + ) .await .unwrap(); buffer_a diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0aac59db43..226291722c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -53,6 +53,7 @@ pub struct Buffer { saved_version: clock::Global, saved_version_fingerprint: String, saved_mtime: SystemTime, + newline_style: NewlineStyle, transaction_depth: usize, was_dirty_before_starting_transaction: Option, language: Option>, @@ -97,6 +98,12 @@ pub enum IndentKind { Tab, } +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum NewlineStyle { + Unix, + Windows, +} + #[derive(Clone, Debug)] struct SelectionSet { line_mode: bool, @@ -194,6 +201,7 @@ pub trait File: Send + Sync { buffer_id: u64, text: Rope, version: clock::Global, + newline_style: NewlineStyle, cx: &mut MutableAppContext, ) -> Task>; @@ -309,13 +317,12 @@ impl Buffer { base_text: T, cx: &mut ModelContext, ) -> Self { + let history = History::new(base_text.into()); + let newline_style = NewlineStyle::detect(&history.base_text); Self::build( - TextBuffer::new( - replica_id, - cx.model_id() as u64, - History::new(base_text.into()), - ), + TextBuffer::new(replica_id, cx.model_id() as u64, history), None, + newline_style, ) } @@ -325,13 +332,12 @@ impl Buffer { file: Arc, cx: &mut ModelContext, ) -> Self { + let history = History::new(base_text.into()); + let newline_style = NewlineStyle::detect(&history.base_text); Self::build( - TextBuffer::new( - replica_id, - cx.model_id() as u64, - History::new(base_text.into()), - ), + TextBuffer::new(replica_id, cx.model_id() as u64, history), Some(file), + newline_style, ) } @@ -346,7 +352,9 @@ impl Buffer { message.id, History::new(Arc::from(message.base_text)), ); - let mut this = Self::build(buffer, file); + let newline_style = proto::NewlineStyle::from_i32(message.newline_style) + .ok_or_else(|| anyhow!("missing newline_style"))?; + let mut this = Self::build(buffer, file, NewlineStyle::from_proto(newline_style)); let ops = message .operations .into_iter() @@ -411,6 +419,7 @@ impl Buffer { diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()), diagnostics_timestamp: self.diagnostics_timestamp.value, completion_triggers: self.completion_triggers.clone(), + newline_style: self.newline_style.to_proto() as i32, } } @@ -419,7 +428,7 @@ impl Buffer { self } - fn build(buffer: TextBuffer, file: Option>) -> Self { + fn build(buffer: TextBuffer, file: Option>, newline_style: NewlineStyle) -> Self { let saved_mtime; if let Some(file) = file.as_ref() { saved_mtime = file.mtime(); @@ -435,6 +444,7 @@ impl Buffer { was_dirty_before_starting_transaction: None, text: buffer, file, + newline_style, syntax_tree: Mutex::new(None), parsing_in_background: false, parse_count: 0, @@ -491,7 +501,13 @@ impl Buffer { }; let text = self.as_rope().clone(); let version = self.version(); - let save = file.save(self.remote_id(), text, version, cx.as_mut()); + let save = file.save( + self.remote_id(), + text, + version, + self.newline_style, + cx.as_mut(), + ); cx.spawn(|this, mut cx| async move { let (version, fingerprint, mtime) = save.await?; this.update(&mut cx, |this, cx| { @@ -1492,6 +1508,10 @@ impl Buffer { pub fn completion_triggers(&self) -> &[String] { &self.completion_triggers } + + pub fn newline_style(&self) -> NewlineStyle { + self.newline_style + } } #[cfg(any(test, feature = "test-support"))] @@ -2512,6 +2532,52 @@ impl std::ops::SubAssign for IndentSize { } } +impl NewlineStyle { + fn from_proto(style: proto::NewlineStyle) -> Self { + match style { + proto::NewlineStyle::Unix => Self::Unix, + proto::NewlineStyle::Windows => Self::Windows, + } + } + + fn detect(text: &str) -> Self { + let text = &text[..cmp::min(text.len(), 1000)]; + if let Some(ix) = text.find('\n') { + if ix == 0 || text.as_bytes()[ix - 1] != b'\r' { + Self::Unix + } else { + Self::Windows + } + } else { + Default::default() + } + } + + pub fn as_str(self) -> &'static str { + match self { + NewlineStyle::Unix => "\n", + NewlineStyle::Windows => "\r\n", + } + } + + fn to_proto(self) -> proto::NewlineStyle { + match self { + NewlineStyle::Unix => proto::NewlineStyle::Unix, + NewlineStyle::Windows => proto::NewlineStyle::Windows, + } + } +} + +impl Default for NewlineStyle { + fn default() -> Self { + #[cfg(unix)] + return Self::Unix; + + #[cfg(not(unix))] + return Self::Windows; + } +} + impl Completion { pub fn sort_key(&self) -> (usize, &str) { let kind_key = match self.lsp_completion.kind { diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index d0a10df5a8..698c34853a 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -9,7 +9,7 @@ use rpc::proto; use std::{ops::Range, sync::Arc}; use text::*; -pub use proto::{Buffer, BufferState, SelectionSet}; +pub use proto::{Buffer, BufferState, NewlineStyle, SelectionSet}; pub fn serialize_operation(operation: &Operation) -> proto::Operation { proto::Operation { diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 2eec02d66d..5ae1ab3339 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{Stream, StreamExt}; +use language::NewlineStyle; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ io, @@ -21,7 +22,7 @@ pub trait Fs: Send + Sync { async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn open_sync(&self, path: &Path) -> Result>; async fn load(&self, path: &Path) -> Result; - async fn save(&self, path: &Path, text: &Rope) -> Result<()>; + async fn save(&self, path: &Path, text: &Rope, newline_style: NewlineStyle) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; async fn is_file(&self, path: &Path) -> bool; async fn metadata(&self, path: &Path) -> Result>; @@ -169,12 +170,19 @@ impl Fs for RealFs { Ok(text) } - async fn save(&self, path: &Path, text: &Rope) -> Result<()> { + async fn save(&self, path: &Path, text: &Rope, newline_style: NewlineStyle) -> Result<()> { let buffer_size = text.summary().bytes.min(10 * 1024); let file = smol::fs::File::create(path).await?; let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); + let mut newline = false; for chunk in text.chunks() { - writer.write_all(chunk.as_bytes()).await?; + for line in chunk.split('\n') { + if newline { + writer.write_all(newline_style.as_str().as_bytes()).await?; + } + writer.write_all(line.as_bytes()).await?; + newline = true; + } } writer.flush().await?; Ok(()) @@ -646,7 +654,7 @@ impl Fs for FakeFs { Ok(text.clone()) } - async fn save(&self, path: &Path, text: &Rope) -> Result<()> { + async fn save(&self, path: &Path, text: &Rope, newline_style: NewlineStyle) -> Result<()> { self.simulate_random_delay().await; let mut state = self.state.lock().await; let path = normalize_path(path); @@ -655,7 +663,11 @@ impl Fs for FakeFs { if entry.metadata.is_dir { Err(anyhow!("cannot overwrite a directory with a file")) } else { - entry.content = Some(text.chunks().collect()); + entry.content = Some( + text.chunks() + .map(|chunk| chunk.replace('\n', newline_style.as_str())) + .collect(), + ); entry.metadata.mtime = SystemTime::now(); state.emit_event(&[path]).await; Ok(()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e8ee4810a3..9f5ca8f99d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -6054,7 +6054,7 @@ mod tests { use gpui::{executor::Deterministic, test::subscribe}; use language::{ tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, - OffsetRangeExt, Point, ToPoint, + NewlineStyle, OffsetRangeExt, Point, ToPoint, }; use lsp::Url; use serde_json::json; @@ -8547,9 +8547,13 @@ mod tests { assert!(!buffer.has_conflict()); }); let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; - fs.save("/dir/the-file".as_ref(), &new_contents.into()) - .await - .unwrap(); + fs.save( + "/dir/the-file".as_ref(), + &new_contents.into(), + NewlineStyle::Unix, + ) + .await + .unwrap(); // Because the buffer was not modified, it is reloaded from disk. Its // contents are edited according to the diff between the old and new @@ -8584,6 +8588,7 @@ mod tests { fs.save( "/dir/the-file".as_ref(), &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), + NewlineStyle::Unix, ) .await .unwrap(); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b472a28771..35c9f46851 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -24,7 +24,7 @@ use gpui::{ }; use language::{ proto::{deserialize_version, serialize_version}, - Buffer, DiagnosticEntry, PointUtf16, Rope, + Buffer, DiagnosticEntry, NewlineStyle, PointUtf16, Rope, }; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -595,7 +595,7 @@ impl LocalWorktree { let text = buffer.as_rope().clone(); let fingerprint = text.fingerprint(); let version = buffer.version(); - let save = self.write_file(path, text, cx); + let save = self.write_file(path, text, buffer.newline_style(), cx); let handle = cx.handle(); cx.as_mut().spawn(|mut cx| async move { let entry = save.await?; @@ -636,9 +636,10 @@ impl LocalWorktree { &self, path: impl Into>, text: Rope, + newline_style: NewlineStyle, cx: &mut ModelContext, ) -> Task> { - self.write_entry_internal(path, Some(text), cx) + self.write_entry_internal(path, Some((text, newline_style)), cx) } pub fn delete_entry( @@ -754,7 +755,7 @@ impl LocalWorktree { fn write_entry_internal( &self, path: impl Into>, - text_if_file: Option, + text_if_file: Option<(Rope, NewlineStyle)>, cx: &mut ModelContext, ) -> Task> { let path = path.into(); @@ -763,8 +764,8 @@ impl LocalWorktree { let fs = self.fs.clone(); let abs_path = abs_path.clone(); async move { - if let Some(text) = text_if_file { - fs.save(&abs_path, &text).await + if let Some((text, newline_style)) = text_if_file { + fs.save(&abs_path, &text, newline_style).await } else { fs.create_dir(&abs_path).await } @@ -1653,6 +1654,7 @@ impl language::File for File { buffer_id: u64, text: Rope, version: clock::Global, + newline_style: NewlineStyle, cx: &mut MutableAppContext, ) -> Task> { self.worktree.update(cx, |worktree, cx| match worktree { @@ -1660,7 +1662,7 @@ impl language::File for File { let rpc = worktree.client.clone(); let project_id = worktree.share.as_ref().map(|share| share.project_id); let fingerprint = text.fingerprint(); - let save = worktree.write_file(self.path.clone(), text, cx); + let save = worktree.write_file(self.path.clone(), text, newline_style, cx); cx.background().spawn(async move { let entry = save.await?; if let Some(project_id) = project_id { @@ -2841,6 +2843,7 @@ mod tests { tree.as_local().unwrap().write_file( Path::new("tracked-dir/file.txt"), "hello".into(), + Default::default(), cx, ) }) @@ -2850,6 +2853,7 @@ mod tests { tree.as_local().unwrap().write_file( Path::new("ignored-dir/file.txt"), "world".into(), + Default::default(), cx, ) }) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6325e5107d..a25e19a526 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -810,6 +810,12 @@ message BufferState { repeated Diagnostic diagnostics = 6; uint32 diagnostics_timestamp = 7; repeated string completion_triggers = 8; + NewlineStyle newline_style = 9; +} + +enum NewlineStyle { + Unix = 0; + Windows = 1; } message SelectionSet { diff --git a/crates/zed/src/settings_file.rs b/crates/zed/src/settings_file.rs index 1a7a2e2972..65cac2a496 100644 --- a/crates/zed/src/settings_file.rs +++ b/crates/zed/src/settings_file.rs @@ -117,6 +117,7 @@ mod tests { } "# .into(), + Default::default(), ) .await .unwrap(); @@ -174,6 +175,7 @@ mod tests { } "# .into(), + Default::default(), ) .await .unwrap();