@@ -1,13 +1,17 @@
1use crate::{
1use crate::{
2− find_root, jsonrpc,
2+ find_lsp_workspace, jsonrpc,
3 transport::{Payload, Transport},
3 transport::{Payload, Transport},
4 Call, Error, OffsetEncoding, Result,
4 Call, Error, OffsetEncoding, Result,
7−use helix_core::{ChangeSet, Rope};
7+use helix_core::{find_workspace, ChangeSet, Rope};
8use helix_loader::{self, VERSION_AND_GIT_HASH};
8use helix_loader::{self, VERSION_AND_GIT_HASH};
10+ notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
9−use lsp::PositionEncodingKind;
11+ PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
10use lsp_types as lsp;
13use lsp_types as lsp;
14+use parking_lot::Mutex;
11use serde::Deserialize;
15use serde::Deserialize;
12use serde_json::Value;
16use serde_json::Value;
13use std::future::Future;
17use std::future::Future;
@@ -26,6 +30,17 @@ use tokio::{
33+fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
34+ lsp::WorkspaceFolder {
37+ .and_then(|segments| segments.last())
38+ .map(|basename| basename.to_string())
39+ .unwrap_or_default(),
29#[derive(Debug)]
44#[derive(Debug)]
30pub struct Client {
45pub struct Client {
31 id: usize,
46 id: usize,
@@ -36,11 +51,120 @@ pub struct Client {
36 config: Option<Value>,
51 config: Option<Value>,
37 root_path: std::path::PathBuf,
52 root_path: std::path::PathBuf,
38 root_uri: Option<lsp::Url>,
53 root_uri: Option<lsp::Url>,
39− workspace_folders: Vec<lsp::WorkspaceFolder>,
54+ workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
55+ initalize_notify: Arc<Notify>,
56+ /// workspace folders added while the server is still initalizing
40 req_timeout: u64,
57 req_timeout: u64,
43impl Client {
60impl Client {
63+ root_markers: &[String],
64+ manual_roots: &[PathBuf],
65+ doc_path: Option<&std::path::PathBuf>,
66+ may_support_workspace: bool,
68+ let (workspace, workspace_is_cwd) = find_workspace();
69+ let root = find_lsp_workspace(
71+ .and_then(|x| x.parent().and_then(|x| x.to_str()))
80+ .and_then(|root| lsp::Url::from_file_path(root).ok());
82+ if self.root_path == root.unwrap_or(workspace)
83+ || root_uri.as_ref().map_or(false, |root_uri| {
84+ self.workspace_folders
87+ .any(|workspace| &workspace.uri == root_uri)
90+ // workspace URI is already registered so we can use this client
94+ // this server definitly doesn't support multiple workspace, no need to check capabilities
95+ if !may_support_workspace {
99+ let Some(capabilities) = self.capabilities.get() else {
100+ let client = Arc::clone(self);
101+ // initalization hasn't finished yet, deal with this new root later
102+ // TODO: In the edgecase that a **new root** is added
103+ // for an LSP that **doesn't support workspace_folders** before initaliation is finished
104+ // the new roots are ignored.
105+ // That particular edgecase would require retroactively spawning new LSP
106+ // clients and therefore also require us to retroactively update the corresponding
107+ // documents LSP client handle. It's doable but a pretty weird edgecase so let's
108+ // wait and see if anyone ever runs into it.
109+ tokio::spawn(async move {
110+ client.initalize_notify.notified().await;
111+ if let Some(workspace_folders_caps) = client
115+ .and_then(|cap| cap.workspace_folders.as_ref())
116+ .filter(|cap| cap.supported.unwrap_or(false))
118+ client.add_workspace_folder(
120+ &workspace_folders_caps.change_notifications,
127+ if let Some(workspace_folders_caps) = capabilities
130+ .and_then(|cap| cap.workspace_folders.as_ref())
131+ .filter(|cap| cap.supported.unwrap_or(false))
133+ self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications);
136+ // the server doesn't support multi workspaces, we need a new client
141+ fn add_workspace_folder(
143+ root_uri: Option<lsp::Url>,
144+ change_notifications: &Option<OneOf<bool, String>>,
146+ // root_uri is None just means that there isn't really any LSP workspace
147+ // associated with this file. For servers that support multiple workspaces
148+ // there is just one server so we can always just use that shared instance.
149+ // No need to add a new workspace root here as there is no logical root for this file
150+ // let the server deal with this
151+ let Some(root_uri) = root_uri else {
155+ // server supports workspace folders, let's add the new root to the list
156+ self.workspace_folders
158+ .push(workspace_for_uri(root_uri.clone()));
159+ if &Some(OneOf::Left(false)) == change_notifications {
160+ // server specifically opted out of DidWorkspaceChange notifications
161+ // let's assume the server will request the workspace folders itself
162+ // and that we can therefore reuse the client (but are done now)
165+ tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
44 #[allow(clippy::type_complexity)]
168 #[allow(clippy::type_complexity)]
45 #[allow(clippy::too_many_arguments)]
169 #[allow(clippy::too_many_arguments)]
46 pub fn start(
170 pub fn start(
@@ -76,30 +200,25 @@ impl Client {
77 let (server_rx, server_tx, initialize_notify) =
201 let (server_rx, server_tx, initialize_notify) =
78 Transport::start(reader, writer, stderr, id);
202 Transport::start(reader, writer, stderr, id);
79−
203+ let (workspace, workspace_is_cwd) = find_workspace();
80− let root_path = find_root(
204+ let root = find_lsp_workspace(
82 .and_then(|x| x.parent().and_then(|x| x.to_str()))
206 .and_then(|x| x.parent().and_then(|x| x.to_str()))
83 .unwrap_or("."),
207 .unwrap_or("."),
84 root_markers,
208 root_markers,
85 manual_roots,
209 manual_roots,
214+ // `root_uri` and `workspace_folder` can be empty in case there is no workspace
215+ // `root_url` can not, use `workspace` as a fallback
216+ let root_path = root.clone().unwrap_or_else(|| workspace.clone());
88− let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();
217+ let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
90− // TODO: support multiple workspace folders
91 let workspace_folders = root_uri
219 let workspace_folders = root_uri
93− .map(|root| {
221+ .map(|root| vec![workspace_for_uri(root)])
94− vec![lsp::WorkspaceFolder {
97− .and_then(|segments| segments.last())
98− .map(|basename| basename.to_string())
99− .unwrap_or_default(),
103 .unwrap_or_default();
222 .unwrap_or_default();
105 let client = Self {
224 let client = Self {
@@ -110,10 +229,10 @@ impl Client {
110 capabilities: OnceCell::new(),
229 capabilities: OnceCell::new(),
112 req_timeout,
231 req_timeout,
114 root_path,
232 root_path,
115 root_uri,
233 root_uri,
116− workspace_folders,
234+ workspace_folders: Mutex::new(workspace_folders),
235+ initalize_notify: initialize_notify.clone(),
119 Ok((client, server_rx, initialize_notify))
238 Ok((client, server_rx, initialize_notify))
@@ -169,8 +288,10 @@ impl Client {
169 self.config.as_ref()
288 self.config.as_ref()
172− pub fn workspace_folders(&self) -> &[lsp::WorkspaceFolder] {
291+ pub async fn workspace_folders(
293+ ) -> parking_lot::MutexGuard<'_, Vec<lsp::WorkspaceFolder>> {
173− &self.workspace_folders
294+ self.workspace_folders.lock()
176 /// Execute a RPC request on the language server.
297 /// Execute a RPC request on the language server.
@@ -298,7 +419,7 @@ impl Client {
298 #[allow(deprecated)]
419 #[allow(deprecated)]
299 let params = lsp::InitializeParams {
420 let params = lsp::InitializeParams {
300 process_id: Some(std::process::id()),
421 process_id: Some(std::process::id()),
301− workspace_folders: Some(self.workspace_folders.clone()),
422+ workspace_folders: Some(self.workspace_folders.lock().clone()),
302 // root_path is obsolete, but some clients like pyright still use it so we specify both.
423 // root_path is obsolete, but some clients like pyright still use it so we specify both.
303 // clients will prefer _uri if possible
424 // clients will prefer _uri if possible
304 root_path: self.root_path.to_str().map(|path| path.to_owned()),
425 root_path: self.root_path.to_str().map(|path| path.to_owned()),
@@ -469,6 +590,16 @@ impl Client {
593+ pub fn did_change_workspace(
595+ added: Vec<WorkspaceFolder>,
596+ removed: Vec<WorkspaceFolder>,
597+ ) -> impl Future<Output = Result<()>> {
598+ self.notify::<DidChangeWorkspaceFolders>(DidChangeWorkspaceFoldersParams {
599+ event: WorkspaceFoldersChangeEvent { added, removed },
472 // -------------------------------------------------------------------------------------------
603 // -------------------------------------------------------------------------------------------
473 // Text document
604 // Text document
474 // -------------------------------------------------------------------------------------------
605 // -------------------------------------------------------------------------------------------