Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--helix-lsp/src/client.rs101
-rw-r--r--helix-lsp/src/file_operations.rs13
-rw-r--r--helix-view/src/editor.rs90
-rw-r--r--helix-view/src/handlers/lsp.rs42
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)?;