aboutsummaryrefslogtreecommitdiff
path: root/helix-lsp/src/client.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-lsp/src/client.rs')
-rw-r--r--helix-lsp/src/client.rs177
1 files changed, 154 insertions, 23 deletions
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 34e4c346..3dab6bc5 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -1,13 +1,17 @@
use crate::{
- find_root, jsonrpc,
+ find_lsp_workspace, jsonrpc,
transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result,
};
-use helix_core::{ChangeSet, Rope};
+use helix_core::{find_workspace, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
-use lsp::PositionEncodingKind;
+use lsp::{
+ notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
+ PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
+};
use lsp_types as lsp;
+use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value;
use std::future::Future;
@@ -26,6 +30,17 @@ use tokio::{
},
};
+fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
+ lsp::WorkspaceFolder {
+ name: uri
+ .path_segments()
+ .and_then(|segments| segments.last())
+ .map(|basename| basename.to_string())
+ .unwrap_or_default(),
+ uri,
+ }
+}
+
#[derive(Debug)]
pub struct Client {
id: usize,
@@ -36,11 +51,120 @@ pub struct Client {
config: Option<Value>,
root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>,
- workspace_folders: Vec<lsp::WorkspaceFolder>,
+ workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
+ initalize_notify: Arc<Notify>,
+ /// workspace folders added while the server is still initalizing
req_timeout: u64,
}
impl Client {
+ pub fn try_add_doc(
+ self: &Arc<Self>,
+ root_markers: &[String],
+ manual_roots: &[PathBuf],
+ doc_path: Option<&std::path::PathBuf>,
+ may_support_workspace: bool,
+ ) -> bool {
+ let (workspace, workspace_is_cwd) = find_workspace();
+ let root = find_lsp_workspace(
+ doc_path
+ .and_then(|x| x.parent().and_then(|x| x.to_str()))
+ .unwrap_or("."),
+ root_markers,
+ manual_roots,
+ &workspace,
+ workspace_is_cwd,
+ );
+ let root_uri = root
+ .as_ref()
+ .and_then(|root| lsp::Url::from_file_path(root).ok());
+
+ if self.root_path == root.unwrap_or(workspace)
+ || root_uri.as_ref().map_or(false, |root_uri| {
+ self.workspace_folders
+ .lock()
+ .iter()
+ .any(|workspace| &workspace.uri == root_uri)
+ })
+ {
+ // workspace URI is already registered so we can use this client
+ return true;
+ }
+
+ // this server definitly doesn't support multiple workspace, no need to check capabilities
+ if !may_support_workspace {
+ return false;
+ }
+
+ let Some(capabilities) = self.capabilities.get() else {
+ let client = Arc::clone(self);
+ // initalization hasn't finished yet, deal with this new root later
+ // TODO: In the edgecase that a **new root** is added
+ // for an LSP that **doesn't support workspace_folders** before initaliation is finished
+ // the new roots are ignored.
+ // That particular edgecase would require retroactively spawning new LSP
+ // clients and therefore also require us to retroactively update the corresponding
+ // documents LSP client handle. It's doable but a pretty weird edgecase so let's
+ // wait and see if anyone ever runs into it.
+ tokio::spawn(async move {
+ client.initalize_notify.notified().await;
+ if let Some(workspace_folders_caps) = client
+ .capabilities()
+ .workspace
+ .as_ref()
+ .and_then(|cap| cap.workspace_folders.as_ref())
+ .filter(|cap| cap.supported.unwrap_or(false))
+ {
+ client.add_workspace_folder(
+ root_uri,
+ &workspace_folders_caps.change_notifications,
+ );
+ }
+ });
+ return true;
+ };
+
+ if let Some(workspace_folders_caps) = capabilities
+ .workspace
+ .as_ref()
+ .and_then(|cap| cap.workspace_folders.as_ref())
+ .filter(|cap| cap.supported.unwrap_or(false))
+ {
+ self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications);
+ true
+ } else {
+ // the server doesn't support multi workspaces, we need a new client
+ false
+ }
+ }
+
+ fn add_workspace_folder(
+ &self,
+ root_uri: Option<lsp::Url>,
+ change_notifications: &Option<OneOf<bool, String>>,
+ ) {
+ // root_uri is None just means that there isn't really any LSP workspace
+ // associated with this file. For servers that support multiple workspaces
+ // there is just one server so we can always just use that shared instance.
+ // No need to add a new workspace root here as there is no logical root for this file
+ // let the server deal with this
+ let Some(root_uri) = root_uri else {
+ return;
+ };
+
+ // server supports workspace folders, let's add the new root to the list
+ self.workspace_folders
+ .lock()
+ .push(workspace_for_uri(root_uri.clone()));
+ if &Some(OneOf::Left(false)) == change_notifications {
+ // server specifically opted out of DidWorkspaceChange notifications
+ // let's assume the server will request the workspace folders itself
+ // and that we can therefore reuse the client (but are done now)
+ return;
+ }
+ tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
+ }
+
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
pub fn start(
@@ -76,30 +200,25 @@ impl Client {
let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id);
-
- let root_path = find_root(
+ let (workspace, workspace_is_cwd) = find_workspace();
+ let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers,
manual_roots,
+ &workspace,
+ workspace_is_cwd,
);
- let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();
+ // `root_uri` and `workspace_folder` can be empty in case there is no workspace
+ // `root_url` can not, use `workspace` as a fallback
+ let root_path = root.clone().unwrap_or_else(|| workspace.clone());
+ let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
- // TODO: support multiple workspace folders
let workspace_folders = root_uri
.clone()
- .map(|root| {
- vec![lsp::WorkspaceFolder {
- name: root
- .path_segments()
- .and_then(|segments| segments.last())
- .map(|basename| basename.to_string())
- .unwrap_or_default(),
- uri: root,
- }]
- })
+ .map(|root| vec![workspace_for_uri(root)])
.unwrap_or_default();
let client = Self {
@@ -110,10 +229,10 @@ impl Client {
capabilities: OnceCell::new(),
config,
req_timeout,
-
root_path,
root_uri,
- workspace_folders,
+ workspace_folders: Mutex::new(workspace_folders),
+ initalize_notify: initialize_notify.clone(),
};
Ok((client, server_rx, initialize_notify))
@@ -169,8 +288,10 @@ impl Client {
self.config.as_ref()
}
- pub fn workspace_folders(&self) -> &[lsp::WorkspaceFolder] {
- &self.workspace_folders
+ pub async fn workspace_folders(
+ &self,
+ ) -> parking_lot::MutexGuard<'_, Vec<lsp::WorkspaceFolder>> {
+ self.workspace_folders.lock()
}
/// Execute a RPC request on the language server.
@@ -298,7 +419,7 @@ impl Client {
#[allow(deprecated)]
let params = lsp::InitializeParams {
process_id: Some(std::process::id()),
- workspace_folders: Some(self.workspace_folders.clone()),
+ workspace_folders: Some(self.workspace_folders.lock().clone()),
// root_path is obsolete, but some clients like pyright still use it so we specify both.
// clients will prefer _uri if possible
root_path: self.root_path.to_str().map(|path| path.to_owned()),
@@ -469,6 +590,16 @@ impl Client {
)
}
+ pub fn did_change_workspace(
+ &self,
+ added: Vec<WorkspaceFolder>,
+ removed: Vec<WorkspaceFolder>,
+ ) -> impl Future<Output = Result<()>> {
+ self.notify::<DidChangeWorkspaceFolders>(DidChangeWorkspaceFoldersParams {
+ event: WorkspaceFoldersChangeEvent { added, removed },
+ })
+ }
+
// -------------------------------------------------------------------------------------------
// Text document
// -------------------------------------------------------------------------------------------