From a172c3c5c61a01d3ba01775ddef5519b7e45cb2b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 7 Feb 2022 16:11:40 +0100 Subject: [PATCH] Apply file-system operations coming from an LSP code action Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 7 +- crates/project/src/fs.rs | 274 +++++++++++++++++++++++++++++----- crates/project/src/project.rs | 95 +++++++++++- crates/server/src/rpc.rs | 12 +- crates/zed/src/zed.rs | 7 +- 5 files changed, 346 insertions(+), 49 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 186f117d08..c97f7d0876 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2077,8 +2077,13 @@ impl Editor { Some((buffer, action)) })?; - Some(workspace.project().update(cx, |project, cx| { + let apply_code_actions = workspace.project().update(cx, |project, cx| { project.apply_code_action(buffer, action, cx) + }); + Some(cx.spawn(|workspace, cx| async move { + let buffers = apply_code_actions.await?; + + Ok(()) })) } diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 8ef076abc3..5157d72042 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -13,6 +13,11 @@ use text::Rope; #[async_trait::async_trait] pub trait Fs: Send + Sync { + async fn create_dir(&self, path: &Path) -> Result<()>; + async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; + async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; + async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; + async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn load(&self, path: &Path) -> Result; async fn save(&self, path: &Path, text: &Rope) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; @@ -32,6 +37,24 @@ pub trait Fs: Send + Sync { fn as_fake(&self) -> &FakeFs; } +#[derive(Copy, Clone, Default)] +pub struct CreateOptions { + pub overwrite: bool, + pub ignore_if_exists: bool, +} + +#[derive(Copy, Clone, Default)] +pub struct RenameOptions { + pub overwrite: bool, + pub ignore_if_exists: bool, +} + +#[derive(Copy, Clone, Default)] +pub struct RemoveOptions { + pub recursive: bool, + pub ignore_if_not_exists: bool, +} + #[derive(Clone, Debug)] pub struct Metadata { pub inode: u64, @@ -44,6 +67,60 @@ pub struct RealFs; #[async_trait::async_trait] impl Fs for RealFs { + async fn create_dir(&self, path: &Path) -> Result<()> { + Ok(smol::fs::create_dir_all(path).await?) + } + + async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> { + let mut open_options = smol::fs::OpenOptions::new(); + open_options.create(true); + if options.overwrite { + open_options.truncate(true); + } else if !options.ignore_if_exists { + open_options.create_new(true); + } + open_options.open(path).await?; + Ok(()) + } + + async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> { + if !options.overwrite && smol::fs::metadata(target).await.is_ok() { + if options.ignore_if_exists { + return Ok(()); + } else { + return Err(anyhow!("{target:?} already exists")); + } + } + + smol::fs::rename(source, target).await?; + Ok(()) + } + + async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { + let result = if options.recursive { + smol::fs::remove_dir_all(path).await + } else { + smol::fs::remove_dir(path).await + }; + match result { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => { + Ok(()) + } + Err(err) => Err(err)?, + } + } + + async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { + match smol::fs::remove_file(path).await { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => { + Ok(()) + } + Err(err) => Err(err)?, + } + } + async fn load(&self, path: &Path) -> Result { let mut file = smol::fs::File::open(path).await?; let mut text = String::new(); @@ -162,15 +239,19 @@ impl FakeFsState { } } - async fn emit_event(&mut self, paths: &[&Path]) { + async fn emit_event(&mut self, paths: I) + where + I: IntoIterator, + T: Into, + { use postage::prelude::Sink as _; let events = paths - .iter() + .into_iter() .map(|path| fsevent::Event { event_id: 0, flags: fsevent::StreamFlags::empty(), - path: path.to_path_buf(), + path: path.into(), }) .collect(); @@ -292,46 +373,163 @@ impl FakeFs { } .boxed() } - - pub async fn remove(&self, path: &Path) -> Result<()> { - let mut state = self.state.lock().await; - state.validate_path(path)?; - state.entries.retain(|path, _| !path.starts_with(path)); - state.emit_event(&[path]).await; - Ok(()) - } - - pub async fn rename(&self, source: &Path, target: &Path) -> Result<()> { - let mut state = self.state.lock().await; - state.validate_path(source)?; - state.validate_path(target)?; - if state.entries.contains_key(target) { - Err(anyhow!("target path already exists")) - } else { - let mut removed = Vec::new(); - state.entries.retain(|path, entry| { - if let Ok(relative_path) = path.strip_prefix(source) { - removed.push((relative_path.to_path_buf(), entry.clone())); - false - } else { - true - } - }); - - for (relative_path, entry) in removed { - let new_path = target.join(relative_path); - state.entries.insert(new_path, entry); - } - - state.emit_event(&[source, target]).await; - Ok(()) - } - } } #[cfg(any(test, feature = "test-support"))] #[async_trait::async_trait] impl Fs for FakeFs { + async fn create_dir(&self, path: &Path) -> Result<()> { + self.executor.simulate_random_delay().await; + let state = &mut *self.state.lock().await; + let mut ancestor_path = PathBuf::new(); + let mut created_dir_paths = Vec::new(); + for component in path.components() { + ancestor_path.push(component); + let entry = state + .entries + .entry(ancestor_path.clone()) + .or_insert_with(|| { + let inode = state.next_inode; + state.next_inode += 1; + created_dir_paths.push(ancestor_path.clone()); + FakeFsEntry { + metadata: Metadata { + inode, + mtime: SystemTime::now(), + is_dir: true, + is_symlink: false, + }, + content: None, + } + }); + if !entry.metadata.is_dir { + return Err(anyhow!( + "cannot create directory because {:?} is a file", + ancestor_path + )); + } + } + state.emit_event(&created_dir_paths).await; + + Ok(()) + } + + async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> { + self.executor.simulate_random_delay().await; + let mut state = self.state.lock().await; + state.validate_path(path)?; + if let Some(entry) = state.entries.get_mut(path) { + if entry.metadata.is_dir || entry.metadata.is_symlink { + return Err(anyhow!( + "cannot create file because {:?} is a dir or a symlink", + path + )); + } + + if options.overwrite { + entry.metadata.mtime = SystemTime::now(); + entry.content = Some(Default::default()); + } else if !options.ignore_if_exists { + return Err(anyhow!( + "cannot create file because {:?} already exists", + path + )); + } + } else { + let inode = state.next_inode; + state.next_inode += 1; + let entry = FakeFsEntry { + metadata: Metadata { + inode, + mtime: SystemTime::now(), + is_dir: false, + is_symlink: false, + }, + content: Some(Default::default()), + }; + state.entries.insert(path.to_path_buf(), entry); + } + state.emit_event(&[path]).await; + + Ok(()) + } + + async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> { + let mut state = self.state.lock().await; + state.validate_path(source)?; + state.validate_path(target)?; + + if !options.overwrite && state.entries.contains_key(target) { + if options.ignore_if_exists { + return Ok(()); + } else { + return Err(anyhow!("{target:?} already exists")); + } + } + + let mut removed = Vec::new(); + state.entries.retain(|path, entry| { + if let Ok(relative_path) = path.strip_prefix(source) { + removed.push((relative_path.to_path_buf(), entry.clone())); + false + } else { + true + } + }); + + for (relative_path, entry) in removed { + let new_path = target.join(relative_path); + state.entries.insert(new_path, entry); + } + + state.emit_event(&[source, target]).await; + Ok(()) + } + + async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { + let mut state = self.state.lock().await; + state.validate_path(path)?; + if let Some(entry) = state.entries.get(path) { + if !entry.metadata.is_dir { + return Err(anyhow!("cannot remove {path:?} because it is not a dir")); + } + + if !options.recursive { + let descendants = state + .entries + .keys() + .filter(|path| path.starts_with(path)) + .count(); + if descendants > 1 { + return Err(anyhow!("{path:?} is not empty")); + } + } + + state.entries.retain(|path, _| !path.starts_with(path)); + state.emit_event(&[path]).await; + } else if !options.ignore_if_not_exists { + return Err(anyhow!("{path:?} does not exist")); + } + + Ok(()) + } + + async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { + let mut state = self.state.lock().await; + state.validate_path(path)?; + if let Some(entry) = state.entries.get(path) { + if entry.metadata.is_dir { + return Err(anyhow!("cannot remove {path:?} because it is not a file")); + } + + state.entries.remove(path); + state.emit_event(&[path]).await; + } else if !options.ignore_if_not_exists { + return Err(anyhow!("{path:?} does not exist")); + } + Ok(()) + } + async fn load(&self, path: &Path) -> Result { self.executor.simulate_random_delay().await; let state = self.state.lock().await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4bac937e65..378a126311 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1156,7 +1156,7 @@ impl Project { buffer: ModelHandle, mut action: CodeAction, cx: &mut ModelContext, - ) -> Task> { + ) -> Task, Vec>>>> { if self.is_local() { let buffer = buffer.read(cx); let server = if let Some(language_server) = buffer.language_server() { @@ -1165,6 +1165,7 @@ impl Project { return Task::ready(Ok(Default::default())); }; let position = action.position.to_point_utf16(buffer).to_lsp_position(); + let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { let range = action @@ -1178,9 +1179,68 @@ impl Project { let action = server .request::(action.lsp_action) .await?; - let edit = action - .edit - .ok_or_else(|| anyhow!("code action has no edit")); + + let mut operations = Vec::new(); + match action.edit.and_then(|e| e.document_changes) { + Some(lsp::DocumentChanges::Edits(edits)) => { + operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)) + } + Some(lsp::DocumentChanges::Operations(ops)) => operations = ops, + None => {} + } + + for operation in operations { + match operation { + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { + let path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + + if let Some(parent_path) = path.parent() { + fs.create_dir(parent_path).await?; + } + if path.ends_with("/") { + fs.create_dir(&path).await?; + } else { + fs.create_file( + &path, + op.options.map(Into::into).unwrap_or_default(), + ) + .await?; + } + } + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { + let source = op + .old_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let target = op + .new_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + fs.rename( + &source, + &target, + op.options.map(Into::into).unwrap_or_default(), + ) + .await?; + } + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { + let path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let options = op.options.map(Into::into).unwrap_or_default(); + if path.ends_with("/") { + fs.remove_dir(&path, options).await?; + } else { + fs.remove_file(&path, options).await?; + } + } + lsp::DocumentChangeOperation::Edit(edit) => todo!(), + } + } // match edit { // Ok(edit) => edit., // Err(_) => todo!(), @@ -2263,6 +2323,33 @@ impl> From<(WorktreeId, P)> for ProjectPath { } } +impl From for fs::CreateOptions { + fn from(options: lsp::CreateFileOptions) -> Self { + Self { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + } + } +} + +impl From for fs::RenameOptions { + fn from(options: lsp::RenameFileOptions) -> Self { + Self { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + } + } +} + +impl From for fs::RemoveOptions { + fn from(options: lsp::DeleteFileOptions) -> Self { + Self { + recursive: options.recursive.unwrap_or(false), + ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), + } + } +} + #[cfg(test)] mod tests { use super::{Event, *}; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 172f77dd6b..98006e7865 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1607,10 +1607,14 @@ mod tests { buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await; // Make changes on host's file system, see those changes on guest worktrees. - fs.rename("/a/file1".as_ref(), "/a/file1-renamed".as_ref()) - .await - .unwrap(); - fs.rename("/a/file2".as_ref(), "/a/file3".as_ref()) + fs.rename( + "/a/file1".as_ref(), + "/a/file1-renamed".as_ref(), + Default::default(), + ) + .await + .unwrap(); + fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) .await .unwrap(); fs.insert_file(Path::new("/a/file4"), "4".into()) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d51376afb0..791c236ec6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -126,7 +126,7 @@ mod tests { use super::*; use editor::{DisplayPoint, Editor}; use gpui::{MutableAppContext, TestAppContext, ViewHandle}; - use project::ProjectPath; + use project::{Fs, ProjectPath}; use serde_json::json; use std::{ collections::HashSet, @@ -817,7 +817,10 @@ mod tests { .active_pane() .update(cx, |pane, cx| pane.close_item(editor2.id(), cx)); drop(editor2); - app_state.fs.as_fake().remove(Path::new("/root/a/file2")) + app_state + .fs + .as_fake() + .remove_file(Path::new("/root/a/file2"), Default::default()) }) .await .unwrap();