diff options
Diffstat (limited to 'helix-lsp/src/client.rs')
-rw-r--r-- | helix-lsp/src/client.rs | 177 |
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 // ------------------------------------------------------------------------------------------- |