Unnamed repository; edit this file 'description' to name the repository.
| -rw-r--r-- | helix-lsp/src/client.rs | 101 | ||||
| -rw-r--r-- | helix-lsp/src/file_operations.rs | 13 | ||||
| -rw-r--r-- | helix-view/src/editor.rs | 90 | ||||
| -rw-r--r-- | helix-view/src/handlers/lsp.rs | 42 |
4 files changed, 192 insertions, 54 deletions
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index ee67201b..ad19efae 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -616,8 +616,12 @@ impl Client { relative_pattern_support: Some(true), }), file_operations: Some(lsp::WorkspaceFileOperationsClientCapabilities { + will_create: Some(true), + did_create: Some(true), will_rename: Some(true), did_rename: Some(true), + will_delete: Some(true), + did_delete: Some(true), ..Default::default() }), diagnostic: Some(lsp::DiagnosticWorkspaceClientCapabilities { @@ -808,6 +812,47 @@ impl Client { }) } + fn file_operation_uri(path: &Path, is_dir: bool) -> Option<String> { + let url = if is_dir { + Url::from_directory_path(path) + } else { + Url::from_file_path(path) + }; + Some(url.ok()?.to_string()) + } + + pub fn will_create( + &self, + path: &Path, + is_dir: bool, + ) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceEdit>>>> { + let capabilities = self.file_operations_intests(); + if !capabilities.will_create.has_interest(path, is_dir) { + return None; + } + + let files = vec![lsp::FileCreate { + uri: Self::file_operation_uri(path, is_dir)?, + }]; + Some(self.call_with_timeout::<lsp::request::WillCreateFiles>( + &lsp::CreateFilesParams { files }, + 5, + )) + } + + pub fn did_create(&self, path: &Path, is_dir: bool) -> Option<()> { + let capabilities = self.file_operations_intests(); + if !capabilities.did_create.has_interest(path, is_dir) { + return None; + } + + let files = vec![lsp::FileCreate { + uri: Self::file_operation_uri(path, is_dir)?, + }]; + self.notify::<lsp::notification::DidCreateFiles>(lsp::CreateFilesParams { files }); + Some(()) + } + pub fn will_rename( &self, old_path: &Path, @@ -818,17 +863,9 @@ impl Client { if !capabilities.will_rename.has_interest(old_path, is_dir) { return None; } - let url_from_path = |path| { - let url = if is_dir { - Url::from_directory_path(path) - } else { - Url::from_file_path(path) - }; - Some(url.ok()?.to_string()) - }; let files = vec![lsp::FileRename { - old_uri: url_from_path(old_path)?, - new_uri: url_from_path(new_path)?, + old_uri: Self::file_operation_uri(old_path, is_dir)?, + new_uri: Self::file_operation_uri(new_path, is_dir)?, }]; Some(self.call_with_timeout::<lsp::request::WillRenameFiles>( &lsp::RenameFilesParams { files }, @@ -841,23 +878,47 @@ impl Client { if !capabilities.did_rename.has_interest(new_path, is_dir) { return None; } - let url_from_path = |path| { - let url = if is_dir { - Url::from_directory_path(path) - } else { - Url::from_file_path(path) - }; - Some(url.ok()?.to_string()) - }; let files = vec![lsp::FileRename { - old_uri: url_from_path(old_path)?, - new_uri: url_from_path(new_path)?, + old_uri: Self::file_operation_uri(old_path, is_dir)?, + new_uri: Self::file_operation_uri(new_path, is_dir)?, }]; self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }); Some(()) } + pub fn will_delete( + &self, + path: &Path, + is_dir: bool, + ) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceEdit>>>> { + let capabilities = self.file_operations_intests(); + if !capabilities.will_delete.has_interest(path, is_dir) { + return None; + } + + let files = vec![lsp::FileDelete { + uri: Self::file_operation_uri(path, is_dir)?, + }]; + Some(self.call_with_timeout::<lsp::request::WillDeleteFiles>( + &lsp::DeleteFilesParams { files }, + 5, + )) + } + + pub fn did_delete(&self, path: &Path, is_dir: bool) -> Option<()> { + let capabilities = self.file_operations_intests(); + if !capabilities.did_delete.has_interest(path, is_dir) { + return None; + } + + let files = vec![lsp::FileDelete { + uri: Self::file_operation_uri(path, is_dir)?, + }]; + self.notify::<lsp::notification::DidDeleteFiles>(lsp::DeleteFilesParams { files }); + Some(()) + } + // ------------------------------------------------------------------------------------------- // Text document // ------------------------------------------------------------------------------------------- diff --git a/helix-lsp/src/file_operations.rs b/helix-lsp/src/file_operations.rs index 98ac32a4..903ab303 100644 --- a/helix-lsp/src/file_operations.rs +++ b/helix-lsp/src/file_operations.rs @@ -79,13 +79,12 @@ impl FileOperationFilter { #[derive(Default, Debug)] pub(crate) struct FileOperationsInterest { - // TODO: support other notifications - // did_create: FileOperationFilter, - // will_create: FileOperationFilter, + pub did_create: FileOperationFilter, + pub will_create: FileOperationFilter, pub did_rename: FileOperationFilter, pub will_rename: FileOperationFilter, - // did_delete: FileOperationFilter, - // will_delete: FileOperationFilter, + pub did_delete: FileOperationFilter, + pub will_delete: FileOperationFilter, } impl FileOperationsInterest { @@ -98,8 +97,12 @@ impl FileOperationsInterest { return FileOperationsInterest::default(); }; FileOperationsInterest { + did_create: FileOperationFilter::new(capabilities.did_create.as_ref()), + will_create: FileOperationFilter::new(capabilities.will_create.as_ref()), did_rename: FileOperationFilter::new(capabilities.did_rename.as_ref()), will_rename: FileOperationFilter::new(capabilities.will_rename.as_ref()), + did_delete: FileOperationFilter::new(capabilities.did_delete.as_ref()), + will_delete: FileOperationFilter::new(capabilities.will_delete.as_ref()), } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 62cf3592..104adb39 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1598,6 +1598,96 @@ impl Editor { Ok(()) } + pub fn create_path(&mut self, path: &Path, is_dir: bool) -> io::Result<()> { + let path = canonicalize(path); + let language_servers: Vec<_> = self + .language_servers + .iter_clients() + .filter(|client| client.is_initialized()) + .cloned() + .collect(); + for language_server in language_servers { + let Some(request) = language_server.will_create(&path, is_dir) else { + continue; + }; + let edit = match helix_lsp::block_on(request) { + Ok(edit) => edit.unwrap_or_default(), + Err(err) => { + log::error!("invalid willCreate response: {err:?}"); + continue; + } + }; + if let Err(err) = self.apply_workspace_edit(language_server.offset_encoding(), &edit) { + log::error!("failed to apply workspace edit: {err:?}") + } + } + + if let Some(dir) = path.parent() { + if !dir.is_dir() { + fs::create_dir_all(dir)?; + } + } + if is_dir { + fs::create_dir(&path)?; + } else { + fs::write(&path, [])?; + } + + for ls in self.language_servers.iter_clients() { + if !ls.is_initialized() { + continue; + } + ls.did_create(&path, is_dir); + } + self.language_servers.file_event_handler.file_changed(path); + Ok(()) + } + + pub fn delete_path(&mut self, path: &Path, recursive: bool) -> io::Result<()> { + let path = canonicalize(path); + let is_dir = path.is_dir(); + let language_servers: Vec<_> = self + .language_servers + .iter_clients() + .filter(|client| client.is_initialized()) + .cloned() + .collect(); + for language_server in language_servers { + let Some(request) = language_server.will_delete(&path, is_dir) else { + continue; + }; + let edit = match helix_lsp::block_on(request) { + Ok(edit) => edit.unwrap_or_default(), + Err(err) => { + log::error!("invalid willDelete response: {err:?}"); + continue; + } + }; + if let Err(err) = self.apply_workspace_edit(language_server.offset_encoding(), &edit) { + log::error!("failed to apply workspace edit: {err:?}") + } + } + + if is_dir { + if recursive { + fs::remove_dir_all(&path)?; + } else { + fs::remove_dir(&path)?; + } + } else { + fs::remove_file(&path)?; + } + + for ls in self.language_servers.iter_clients() { + if !ls.is_initialized() { + continue; + } + ls.did_delete(&path, is_dir); + } + self.language_servers.file_event_handler.file_changed(path); + Ok(()) + } + pub fn set_doc_path(&mut self, doc_id: DocumentId, path: &Path) { let doc = doc_mut!(self, &doc_id); let old_path = doc.path(); diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs index b052138f..ecdbfdf7 100644 --- a/helix-view/src/handlers/lsp.rs +++ b/helix-view/src/handlers/lsp.rs @@ -230,7 +230,6 @@ impl Editor { op: &lsp::ResourceOp, ) -> Result<(), ApplyEditErrorKind> { use lsp::ResourceOp; - use std::fs; // NOTE: If `Uri` gets another variant than `Path`, the below `expect`s // may no longer be valid. match op { @@ -241,40 +240,25 @@ impl Editor { !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) }); if !ignore_if_exists || !path.exists() { - // Create directory if it does not exist - if let Some(dir) = path.parent() { - if !dir.is_dir() { - fs::create_dir_all(dir)?; - } - } - - fs::write(path, [])?; - self.language_servers - .file_event_handler - .file_changed(path.to_path_buf()); + self.create_path(path, false)?; } } ResourceOp::Delete(op) => { let uri = Uri::try_from(&op.uri)?; let path = uri.as_path().expect("URIs are valid paths"); - if path.is_dir() { - let recursive = op - .options - .as_ref() - .and_then(|options| options.recursive) - .unwrap_or(false); - - if recursive { - fs::remove_dir_all(path)? - } else { - fs::remove_dir(path)? - } - self.language_servers - .file_event_handler - .file_changed(path.to_path_buf()); - } else if path.is_file() { - fs::remove_file(path)?; + let ignore_if_not_exists = op + .options + .as_ref() + .is_some_and(|options| options.ignore_if_not_exists.unwrap_or(false)); + if ignore_if_not_exists && !path.exists() { + return Ok(()); } + let recursive = op + .options + .as_ref() + .and_then(|options| options.recursive) + .unwrap_or(false); + self.delete_path(path, recursive)?; } ResourceOp::Rename(op) => { let from_uri = Uri::try_from(&op.old_uri)?; |