aboutsummaryrefslogtreecommitdiff
path: root/helix-lsp
diff options
context:
space:
mode:
authorPascal Kuthe2024-01-28 16:34:45 +0000
committerGitHub2024-01-28 16:34:45 +0000
commit87a720c3a13ccc7245f5b0befc008db5bd039032 (patch)
treee0e3f91c516a10d154cd01861e96ae0f50ea3cad /helix-lsp
parentf5b67d9acb89ea54e7226111e3e4d8c3a008144b (diff)
make path changes LSP spec conform (#8949)
Currently, helix implements operations which change the paths of files incorrectly and inconsistently. This PR ensures that we do the following whenever a buffer is renamed (`:move` and workspace edits) * always send did_open/did_close notifications * send will_rename/did_rename requests correctly * send them to all LSP servers not just those that are active for a buffer * also send these requests for paths that are not yet open in a buffer (if triggered from workspace edit). * only send these if the server registered interests in the path * autodetect language, indent, line ending, .. This PR also centralizes the infrastructure for path setting and therefore `:w <path>` benefits from similar fixed (but without didRename)
Diffstat (limited to 'helix-lsp')
-rw-r--r--helix-lsp/src/client.rs83
-rw-r--r--helix-lsp/src/file_operations.rs105
-rw-r--r--helix-lsp/src/lib.rs1
3 files changed, 152 insertions, 37 deletions
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index fb32f6eb..94bad6fa 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -1,4 +1,5 @@
use crate::{
+ file_operations::FileOperationsInterest,
find_lsp_workspace, jsonrpc,
transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result,
@@ -9,20 +10,20 @@ use helix_loader::{self, VERSION_AND_GIT_HASH};
use helix_stdx::path;
use lsp::{
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
- DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, WorkspaceFolder,
- WorkspaceFoldersChangeEvent,
+ DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url,
+ WorkspaceFolder, WorkspaceFoldersChangeEvent,
};
use lsp_types as lsp;
use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value;
-use std::future::Future;
-use std::process::Stdio;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use std::{collections::HashMap, path::PathBuf};
+use std::{future::Future, sync::OnceLock};
+use std::{path::Path, process::Stdio};
use tokio::{
io::{BufReader, BufWriter},
process::{Child, Command},
@@ -51,6 +52,7 @@ pub struct Client {
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
+ pub(crate) file_operation_interest: OnceLock<FileOperationsInterest>,
config: Option<Value>,
root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>,
@@ -233,6 +235,7 @@ impl Client {
server_tx,
request_counter: AtomicU64::new(0),
capabilities: OnceCell::new(),
+ file_operation_interest: OnceLock::new(),
config,
req_timeout,
root_path,
@@ -278,6 +281,11 @@ impl Client {
.expect("language server not yet initialized!")
}
+ pub(crate) fn file_operations_intests(&self) -> &FileOperationsInterest {
+ self.file_operation_interest
+ .get_or_init(|| FileOperationsInterest::new(self.capabilities()))
+ }
+
/// Client has to be initialized otherwise this function panics
#[inline]
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
@@ -717,27 +725,27 @@ impl Client {
})
}
- pub fn prepare_file_rename(
+ pub fn will_rename(
&self,
- old_uri: &lsp::Url,
- new_uri: &lsp::Url,
+ old_path: &Path,
+ new_path: &Path,
+ is_dir: bool,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
- let capabilities = self.capabilities.get().unwrap();
-
- // Return early if the server does not support willRename feature
- match &capabilities.workspace {
- Some(workspace) => match &workspace.file_operations {
- Some(op) => {
- op.will_rename.as_ref()?;
- }
- _ => return None,
- },
- _ => return None,
+ let capabilities = self.file_operations_intests();
+ 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: old_uri.to_string(),
- new_uri: new_uri.to_string(),
+ old_uri: url_from_path(old_path)?,
+ new_uri: url_from_path(new_path)?,
}];
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
lsp::RenameFilesParams { files },
@@ -751,27 +759,28 @@ impl Client {
})
}
- pub fn did_file_rename(
+ pub fn did_rename(
&self,
- old_uri: &lsp::Url,
- new_uri: &lsp::Url,
+ old_path: &Path,
+ new_path: &Path,
+ is_dir: bool,
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
- let capabilities = self.capabilities.get().unwrap();
-
- // Return early if the server does not support DidRename feature
- match &capabilities.workspace {
- Some(workspace) => match &workspace.file_operations {
- Some(op) => {
- op.did_rename.as_ref()?;
- }
- _ => return None,
- },
- _ => return None,
+ let capabilities = self.file_operations_intests();
+ 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: old_uri.to_string(),
- new_uri: new_uri.to_string(),
+ old_uri: url_from_path(old_path)?,
+ new_uri: url_from_path(new_path)?,
}];
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
}
diff --git a/helix-lsp/src/file_operations.rs b/helix-lsp/src/file_operations.rs
new file mode 100644
index 00000000..98ac32a4
--- /dev/null
+++ b/helix-lsp/src/file_operations.rs
@@ -0,0 +1,105 @@
+use std::path::Path;
+
+use globset::{GlobBuilder, GlobSet};
+
+use crate::lsp;
+
+#[derive(Default, Debug)]
+pub(crate) struct FileOperationFilter {
+ dir_globs: GlobSet,
+ file_globs: GlobSet,
+}
+
+impl FileOperationFilter {
+ fn new(capability: Option<&lsp::FileOperationRegistrationOptions>) -> FileOperationFilter {
+ let Some(cap) = capability else {
+ return FileOperationFilter::default();
+ };
+ let mut dir_globs = GlobSet::builder();
+ let mut file_globs = GlobSet::builder();
+ for filter in &cap.filters {
+ // TODO: support other url schemes
+ let is_non_file_schema = filter
+ .scheme
+ .as_ref()
+ .is_some_and(|schema| schema != "file");
+ if is_non_file_schema {
+ continue;
+ }
+ let ignore_case = filter
+ .pattern
+ .options
+ .as_ref()
+ .and_then(|opts| opts.ignore_case)
+ .unwrap_or(false);
+ let mut glob_builder = GlobBuilder::new(&filter.pattern.glob);
+ glob_builder.case_insensitive(!ignore_case);
+ let glob = match glob_builder.build() {
+ Ok(glob) => glob,
+ Err(err) => {
+ log::error!("invalid glob send by LS: {err}");
+ continue;
+ }
+ };
+ match filter.pattern.matches {
+ Some(lsp::FileOperationPatternKind::File) => {
+ file_globs.add(glob);
+ }
+ Some(lsp::FileOperationPatternKind::Folder) => {
+ dir_globs.add(glob);
+ }
+ None => {
+ file_globs.add(glob.clone());
+ dir_globs.add(glob);
+ }
+ };
+ }
+ let file_globs = file_globs.build().unwrap_or_else(|err| {
+ log::error!("invalid globs send by LS: {err}");
+ GlobSet::empty()
+ });
+ let dir_globs = dir_globs.build().unwrap_or_else(|err| {
+ log::error!("invalid globs send by LS: {err}");
+ GlobSet::empty()
+ });
+ FileOperationFilter {
+ dir_globs,
+ file_globs,
+ }
+ }
+
+ pub(crate) fn has_interest(&self, path: &Path, is_dir: bool) -> bool {
+ if is_dir {
+ self.dir_globs.is_match(path)
+ } else {
+ self.file_globs.is_match(path)
+ }
+ }
+}
+
+#[derive(Default, Debug)]
+pub(crate) struct FileOperationsInterest {
+ // TODO: support other notifications
+ // did_create: FileOperationFilter,
+ // will_create: FileOperationFilter,
+ pub did_rename: FileOperationFilter,
+ pub will_rename: FileOperationFilter,
+ // did_delete: FileOperationFilter,
+ // will_delete: FileOperationFilter,
+}
+
+impl FileOperationsInterest {
+ pub fn new(capabilities: &lsp::ServerCapabilities) -> FileOperationsInterest {
+ let capabilities = capabilities
+ .workspace
+ .as_ref()
+ .and_then(|capabilities| capabilities.file_operations.as_ref());
+ let Some(capabilities) = capabilities else {
+ return FileOperationsInterest::default();
+ };
+ FileOperationsInterest {
+ did_rename: FileOperationFilter::new(capabilities.did_rename.as_ref()),
+ will_rename: FileOperationFilter::new(capabilities.will_rename.as_ref()),
+ }
+ }
+}
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 53b2712d..4ce445ae 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -1,5 +1,6 @@
mod client;
pub mod file_event;
+mod file_operations;
pub mod jsonrpc;
pub mod snippet;
mod transport;