lsp/python: Temporarily report just a singular workspace folder instead of all of the roots (#35243)

Piotr Osiewicz and Cole created

Temporarily fixes #29133

Co-authored-by: Cole <cole@zed.dev>

Release Notes:

- python: Zed now reports a slightly different set of workspace folders
for Python projects to work around quirks in handling of multi-lsp
projects with virtual environment. This behavior will be revisited in a
near future.

Co-authored-by: Cole <cole@zed.dev>

Change summary

crates/language/src/language.rs      | 16 +++++++
crates/language_tools/src/lsp_log.rs |  2 
crates/languages/src/python.rs       |  8 +++
crates/lsp/src/lsp.rs                | 64 ++++++++++++++++++++---------
crates/project/src/lsp_store.rs      | 13 ++++-
5 files changed, 76 insertions(+), 27 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -313,6 +313,15 @@ impl Attach {
     }
 }
 
+/// Determines what gets sent out as a workspace folders content
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum WorkspaceFoldersContent {
+    /// Send out a single entry with the root of the workspace.
+    WorktreeRoot,
+    /// Send out a list of subproject roots.
+    SubprojectRoots,
+}
+
 /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
 // e.g. to display a notification or fetch data from the web.
 #[async_trait]
@@ -606,6 +615,13 @@ pub trait LspAdapter: 'static + Send + Sync {
         Attach::Shared
     }
 
+    /// Determines whether a language server supports workspace folders.
+    ///
+    /// And does not trip over itself in the process.
+    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
+        WorkspaceFoldersContent::SubprojectRoots
+    }
+
     fn manifest_name(&self) -> Option<ManifestName> {
         None
     }

crates/language_tools/src/lsp_log.rs 🔗

@@ -867,7 +867,7 @@ impl LspLogView {
             BINARY = server.binary(),
             WORKSPACE_FOLDERS = server
                 .workspace_folders()
-                .iter()
+                .into_iter()
                 .filter_map(|path| path
                     .to_file_path()
                     .ok()

crates/languages/src/python.rs 🔗

@@ -4,13 +4,13 @@ use async_trait::async_trait;
 use collections::HashMap;
 use gpui::{App, Task};
 use gpui::{AsyncApp, SharedString};
-use language::Toolchain;
 use language::ToolchainList;
 use language::ToolchainLister;
 use language::language_settings::language_settings;
 use language::{ContextLocation, LanguageToolchainStore};
 use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
 use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
+use language::{Toolchain, WorkspaceFoldersContent};
 use lsp::LanguageServerBinary;
 use lsp::LanguageServerName;
 use node_runtime::NodeRuntime;
@@ -400,6 +400,9 @@ impl LspAdapter for PythonLspAdapter {
     fn manifest_name(&self) -> Option<ManifestName> {
         Some(SharedString::new_static("pyproject.toml").into())
     }
+    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
+        WorkspaceFoldersContent::WorktreeRoot
+    }
 }
 
 async fn get_cached_server_binary(
@@ -1282,6 +1285,9 @@ impl LspAdapter for PyLspAdapter {
     fn manifest_name(&self) -> Option<ManifestName> {
         Some(SharedString::new_static("pyproject.toml").into())
     }
+    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
+        WorkspaceFoldersContent::WorktreeRoot
+    }
 }
 
 #[cfg(test)]

crates/lsp/src/lsp.rs 🔗

@@ -29,7 +29,7 @@ use std::{
     ffi::{OsStr, OsString},
     fmt,
     io::Write,
-    ops::{Deref, DerefMut},
+    ops::DerefMut,
     path::PathBuf,
     pin::Pin,
     sync::{
@@ -100,7 +100,7 @@ pub struct LanguageServer {
     io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
     output_done_rx: Mutex<Option<barrier::Receiver>>,
     server: Arc<Mutex<Option<Child>>>,
-    workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
+    workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
     root_uri: Url,
 }
 
@@ -307,7 +307,7 @@ impl LanguageServer {
         binary: LanguageServerBinary,
         root_path: &Path,
         code_action_kinds: Option<Vec<CodeActionKind>>,
-        workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
+        workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
         cx: &mut AsyncApp,
     ) -> Result<Self> {
         let working_dir = if root_path.is_dir() {
@@ -381,7 +381,7 @@ impl LanguageServer {
         code_action_kinds: Option<Vec<CodeActionKind>>,
         binary: LanguageServerBinary,
         root_uri: Url,
-        workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
+        workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
         cx: &mut AsyncApp,
         on_unhandled_notification: F,
     ) -> Self
@@ -595,16 +595,26 @@ impl LanguageServer {
     }
 
     pub fn default_initialize_params(&self, pull_diagnostics: bool, cx: &App) -> InitializeParams {
-        let workspace_folders = self
-            .workspace_folders
-            .lock()
-            .iter()
-            .cloned()
-            .map(|uri| WorkspaceFolder {
-                name: Default::default(),
-                uri,
-            })
-            .collect::<Vec<_>>();
+        let workspace_folders = self.workspace_folders.as_ref().map_or_else(
+            || {
+                vec![WorkspaceFolder {
+                    name: Default::default(),
+                    uri: self.root_uri.clone(),
+                }]
+            },
+            |folders| {
+                folders
+                    .lock()
+                    .iter()
+                    .cloned()
+                    .map(|uri| WorkspaceFolder {
+                        name: Default::default(),
+                        uri,
+                    })
+                    .collect()
+            },
+        );
+
         #[allow(deprecated)]
         InitializeParams {
             process_id: None,
@@ -1315,7 +1325,10 @@ impl LanguageServer {
             return;
         }
 
-        let is_new_folder = self.workspace_folders.lock().insert(uri.clone());
+        let Some(workspace_folders) = self.workspace_folders.as_ref() else {
+            return;
+        };
+        let is_new_folder = workspace_folders.lock().insert(uri.clone());
         if is_new_folder {
             let params = DidChangeWorkspaceFoldersParams {
                 event: WorkspaceFoldersChangeEvent {
@@ -1345,7 +1358,10 @@ impl LanguageServer {
         {
             return;
         }
-        let was_removed = self.workspace_folders.lock().remove(&uri);
+        let Some(workspace_folders) = self.workspace_folders.as_ref() else {
+            return;
+        };
+        let was_removed = workspace_folders.lock().remove(&uri);
         if was_removed {
             let params = DidChangeWorkspaceFoldersParams {
                 event: WorkspaceFoldersChangeEvent {
@@ -1360,7 +1376,10 @@ impl LanguageServer {
         }
     }
     pub fn set_workspace_folders(&self, folders: BTreeSet<Url>) {
-        let mut workspace_folders = self.workspace_folders.lock();
+        let Some(workspace_folders) = self.workspace_folders.as_ref() else {
+            return;
+        };
+        let mut workspace_folders = workspace_folders.lock();
 
         let old_workspace_folders = std::mem::take(&mut *workspace_folders);
         let added: Vec<_> = folders
@@ -1389,8 +1408,11 @@ impl LanguageServer {
         }
     }
 
-    pub fn workspace_folders(&self) -> impl Deref<Target = BTreeSet<Url>> + '_ {
-        self.workspace_folders.lock()
+    pub fn workspace_folders(&self) -> BTreeSet<Url> {
+        self.workspace_folders.as_ref().map_or_else(
+            || BTreeSet::from_iter([self.root_uri.clone()]),
+            |folders| folders.lock().clone(),
+        )
     }
 
     pub fn register_buffer(
@@ -1535,7 +1557,7 @@ impl FakeLanguageServer {
             None,
             binary.clone(),
             root,
-            workspace_folders.clone(),
+            Some(workspace_folders.clone()),
             cx,
             |_| {},
         );
@@ -1554,7 +1576,7 @@ impl FakeLanguageServer {
                     None,
                     binary,
                     Self::root_path(),
-                    workspace_folders,
+                    Some(workspace_folders),
                     cx,
                     move |msg| {
                         notifications_tx

crates/project/src/lsp_store.rs 🔗

@@ -46,6 +46,7 @@ use language::{
     DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName,
     LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch,
     PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
+    WorkspaceFoldersContent,
     language_settings::{
         FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
     },
@@ -217,6 +218,7 @@ impl LocalLspStore {
 
         let binary = self.get_language_server_binary(adapter.clone(), delegate.clone(), true, cx);
         let pending_workspace_folders: Arc<Mutex<BTreeSet<Url>>> = Default::default();
+
         let pending_server = cx.spawn({
             let adapter = adapter.clone();
             let server_name = adapter.name.clone();
@@ -242,14 +244,18 @@ impl LocalLspStore {
                     return Ok(server);
                 }
 
+                let code_action_kinds = adapter.code_action_kinds();
                 lsp::LanguageServer::new(
                     stderr_capture,
                     server_id,
                     server_name,
                     binary,
                     &root_path,
-                    adapter.code_action_kinds(),
-                    pending_workspace_folders,
+                    code_action_kinds,
+                    Some(pending_workspace_folders).filter(|_| {
+                        adapter.adapter.workspace_folders_content()
+                            == WorkspaceFoldersContent::SubprojectRoots
+                    }),
                     cx,
                 )
             }
@@ -575,8 +581,7 @@ impl LocalLspStore {
                         };
                         let root = server.workspace_folders();
                         Ok(Some(
-                            root.iter()
-                                .cloned()
+                            root.into_iter()
                                 .map(|uri| WorkspaceFolder {
                                     uri,
                                     name: Default::default(),