@@ -1,13 +1,17 @@
11use crate::{
2− find_root, jsonrpc,
2+ find_lsp_workspace, jsonrpc,
33 transport::{Payload, Transport},
44 Call, Error, OffsetEncoding, Result,
55};
66
7−use helix_core::{ChangeSet, Rope};
7+use helix_core::{find_workspace, ChangeSet, Rope};
88use helix_loader::{self, VERSION_AND_GIT_HASH};
9−use lsp::PositionEncodingKind;
9+use lsp::{
10+ notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
11+ PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
12+};
1013use lsp_types as lsp;
14+use parking_lot::Mutex;
1115use serde::Deserialize;
1216use serde_json::Value;
1317use std::future::Future;
@@ -26,6 +30,17 @@ use tokio::{
2630 },
2731};
2832
33+fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
34+ lsp::WorkspaceFolder {
35+ name: uri
36+ .path_segments()
37+ .and_then(|segments| segments.last())
38+ .map(|basename| basename.to_string())
39+ .unwrap_or_default(),
40+ uri,
41+ }
42+}
43+
2944#[derive(Debug)]
3045pub struct Client {
3146 id: usize,
@@ -36,11 +51,120 @@ pub struct Client {
3651 config: Option<Value>,
3752 root_path: std::path::PathBuf,
3853 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
4057 req_timeout: u64,
4158}
4259
4360impl Client {
61+ pub fn try_add_doc(
62+ self: &Arc<Self>,
63+ root_markers: &[String],
64+ manual_roots: &[PathBuf],
65+ doc_path: Option<&std::path::PathBuf>,
66+ may_support_workspace: bool,
67+ ) -> bool {
68+ let (workspace, workspace_is_cwd) = find_workspace();
69+ let root = find_lsp_workspace(
70+ doc_path
71+ .and_then(|x| x.parent().and_then(|x| x.to_str()))
72+ .unwrap_or("."),
73+ root_markers,
74+ manual_roots,
75+ &workspace,
76+ workspace_is_cwd,
77+ );
78+ let root_uri = root
79+ .as_ref()
80+ .and_then(|root| lsp::Url::from_file_path(root).ok());
81+
82+ if self.root_path == root.unwrap_or(workspace)
83+ || root_uri.as_ref().map_or(false, |root_uri| {
84+ self.workspace_folders
85+ .lock()
86+ .iter()
87+ .any(|workspace| &workspace.uri == root_uri)
88+ })
89+ {
90+ // workspace URI is already registered so we can use this client
91+ return true;
92+ }
93+
94+ // this server definitly doesn't support multiple workspace, no need to check capabilities
95+ if !may_support_workspace {
96+ return false;
97+ }
98+
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
112+ .capabilities()
113+ .workspace
114+ .as_ref()
115+ .and_then(|cap| cap.workspace_folders.as_ref())
116+ .filter(|cap| cap.supported.unwrap_or(false))
117+ {
118+ client.add_workspace_folder(
119+ root_uri,
120+ &workspace_folders_caps.change_notifications,
121+ );
122+ }
123+ });
124+ return true;
125+ };
126+
127+ if let Some(workspace_folders_caps) = capabilities
128+ .workspace
129+ .as_ref()
130+ .and_then(|cap| cap.workspace_folders.as_ref())
131+ .filter(|cap| cap.supported.unwrap_or(false))
132+ {
133+ self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications);
134+ true
135+ } else {
136+ // the server doesn't support multi workspaces, we need a new client
137+ false
138+ }
139+ }
140+
141+ fn add_workspace_folder(
142+ &self,
143+ root_uri: Option<lsp::Url>,
144+ change_notifications: &Option<OneOf<bool, String>>,
145+ ) {
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 {
152+ return;
153+ };
154+
155+ // server supports workspace folders, let's add the new root to the list
156+ self.workspace_folders
157+ .lock()
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)
163+ return;
164+ }
165+ tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
166+ }
167+
44168 #[allow(clippy::type_complexity)]
45169 #[allow(clippy::too_many_arguments)]
46170 pub fn start(
@@ -76,30 +200,25 @@ impl Client {
76200
77201 let (server_rx, server_tx, initialize_notify) =
78202 Transport::start(reader, writer, stderr, id);
79−
80− let root_path = find_root(
203+ let (workspace, workspace_is_cwd) = find_workspace();
204+ let root = find_lsp_workspace(
81205 doc_path
82206 .and_then(|x| x.parent().and_then(|x| x.to_str()))
83207 .unwrap_or("."),
84208 root_markers,
85209 manual_roots,
210+ &workspace,
211+ workspace_is_cwd,
86212 );
87213
88− let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();
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());
217+ let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
89218
90− // TODO: support multiple workspace folders
91219 let workspace_folders = root_uri
92220 .clone()
93− .map(|root| {
94− vec![lsp::WorkspaceFolder {
95− name: root
96− .path_segments()
97− .and_then(|segments| segments.last())
98− .map(|basename| basename.to_string())
99− .unwrap_or_default(),
100− uri: root,
101− }]
102− })
221+ .map(|root| vec![workspace_for_uri(root)])
103222 .unwrap_or_default();
104223
105224 let client = Self {
@@ -110,10 +229,10 @@ impl Client {
110229 capabilities: OnceCell::new(),
111230 config,
112231 req_timeout,
113−
114232 root_path,
115233 root_uri,
116− workspace_folders,
234+ workspace_folders: Mutex::new(workspace_folders),
235+ initalize_notify: initialize_notify.clone(),
117236 };
118237
119238 Ok((client, server_rx, initialize_notify))
@@ -169,8 +288,10 @@ impl Client {
169288 self.config.as_ref()
170289 }
171290
172− pub fn workspace_folders(&self) -> &[lsp::WorkspaceFolder] {
173− &self.workspace_folders
291+ pub async fn workspace_folders(
292+ &self,
293+ ) -> parking_lot::MutexGuard<'_, Vec<lsp::WorkspaceFolder>> {
294+ self.workspace_folders.lock()
174295 }
175296
176297 /// Execute a RPC request on the language server.
@@ -298,7 +419,7 @@ impl Client {
298419 #[allow(deprecated)]
299420 let params = lsp::InitializeParams {
300421 process_id: Some(std::process::id()),
301− workspace_folders: Some(self.workspace_folders.clone()),
422+ workspace_folders: Some(self.workspace_folders.lock().clone()),
302423 // root_path is obsolete, but some clients like pyright still use it so we specify both.
303424 // clients will prefer _uri if possible
304425 root_path: self.root_path.to_str().map(|path| path.to_owned()),
@@ -469,6 +590,16 @@ impl Client {
469590 )
470591 }
471592
593+ pub fn did_change_workspace(
594+ &self,
595+ added: Vec<WorkspaceFolder>,
596+ removed: Vec<WorkspaceFolder>,
597+ ) -> impl Future<Output = Result<()>> {
598+ self.notify::<DidChangeWorkspaceFolders>(DidChangeWorkspaceFoldersParams {
599+ event: WorkspaceFoldersChangeEvent { added, removed },
600+ })
601+ }
602+
472603 // -------------------------------------------------------------------------------------------
473604 // Text document
474605 // -------------------------------------------------------------------------------------------