lsp: Add basic test for server tree toolchain use (#36692)

Piotr Osiewicz created

Closes #ISSUE

Release Notes:

- N/A

Change summary

crates/language/src/toolchain.rs                |   2 
crates/project/src/lsp_store.rs                 |   2 
crates/project/src/manifest_tree/server_tree.rs |   2 
crates/project/src/project_tests.rs             | 260 ++++++++++++++++++
4 files changed, 262 insertions(+), 4 deletions(-)

Detailed changes

crates/language/src/toolchain.rs 🔗

@@ -96,7 +96,7 @@ impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
 }
 
 type DefaultIndex = usize;
-#[derive(Default, Clone)]
+#[derive(Default, Clone, Debug)]
 pub struct ToolchainList {
     pub toolchains: Vec<Toolchain>,
     pub default: Option<DefaultIndex>,

crates/project/src/lsp_store.rs 🔗

@@ -4643,7 +4643,6 @@ impl LspStore {
                     Some((file, language, raw_buffer.remote_id()))
                 })
                 .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible()));
-
             for (file, language, buffer_id) in buffers {
                 let worktree_id = file.worktree_id(cx);
                 let Some(worktree) = local
@@ -4685,7 +4684,6 @@ impl LspStore {
                             cx,
                         )
                         .collect::<Vec<_>>();
-
                     for node in nodes {
                         let server_id = node.server_id_or_init(|disposition| {
                             let path = &disposition.path;

crates/project/src/manifest_tree/server_tree.rs 🔗

@@ -181,6 +181,7 @@ impl LanguageServerTree {
                     &root_path.path,
                     language_name.clone(),
                 );
+
                 (
                     Arc::new(InnerTreeNode::new(
                         adapter.name(),
@@ -408,6 +409,7 @@ impl ServerTreeRebase {
                 if live_node.id.get().is_some() {
                     return Some(node);
                 }
+
                 let disposition = &live_node.disposition;
                 let Some((existing_node, _)) = self
                     .old_contents

crates/project/src/project_tests.rs 🔗

@@ -4,6 +4,7 @@ use crate::{
     Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
     *,
 };
+use async_trait::async_trait;
 use buffer_diff::{
     BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
     DiffHunkStatusKind, assert_hunks,
@@ -21,7 +22,8 @@ use http_client::Url;
 use itertools::Itertools;
 use language::{
     Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter,
-    LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
+    LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider,
+    ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainLister,
     language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
     tree_sitter_rust, tree_sitter_typescript,
 };
@@ -596,6 +598,203 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
+    cx: &mut gpui::TestAppContext,
+) {
+    pub(crate) struct PyprojectTomlManifestProvider;
+
+    impl ManifestProvider for PyprojectTomlManifestProvider {
+        fn name(&self) -> ManifestName {
+            SharedString::new_static("pyproject.toml").into()
+        }
+
+        fn search(
+            &self,
+            ManifestQuery {
+                path,
+                depth,
+                delegate,
+            }: ManifestQuery,
+        ) -> Option<Arc<Path>> {
+            for path in path.ancestors().take(depth) {
+                let p = path.join("pyproject.toml");
+                if delegate.exists(&p, Some(false)) {
+                    return Some(path.into());
+                }
+            }
+
+            None
+        }
+    }
+
+    init_test(cx);
+    let fs = FakeFs::new(cx.executor());
+
+    fs.insert_tree(
+        path!("/the-root"),
+        json!({
+            ".zed": {
+                "settings.json": r#"
+                {
+                    "languages": {
+                        "Python": {
+                            "language_servers": ["ty"]
+                        }
+                    }
+                }"#
+            },
+            "project-a": {
+                ".venv": {},
+                "file.py": "",
+                "pyproject.toml": ""
+            },
+            "project-b": {
+                ".venv": {},
+                "source_file.py":"",
+                "another_file.py": "",
+                "pyproject.toml": ""
+            }
+        }),
+    )
+    .await;
+    cx.update(|cx| {
+        ManifestProvidersStore::global(cx).register(Arc::new(PyprojectTomlManifestProvider))
+    });
+
+    let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    let _fake_python_server = language_registry.register_fake_lsp(
+        "Python",
+        FakeLspAdapter {
+            name: "ty",
+            capabilities: lsp::ServerCapabilities {
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+    );
+
+    language_registry.add(python_lang(fs.clone()));
+    let (first_buffer, _handle) = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer_with_lsp(path!("/the-root/project-a/file.py"), cx)
+        })
+        .await
+        .unwrap();
+    cx.executor().run_until_parked();
+    let servers = project.update(cx, |project, cx| {
+        project.lsp_store.update(cx, |this, cx| {
+            first_buffer.update(cx, |buffer, cx| {
+                this.language_servers_for_local_buffer(buffer, cx)
+                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
+                    .collect::<Vec<_>>()
+            })
+        })
+    });
+    cx.executor().run_until_parked();
+    assert_eq!(servers.len(), 1);
+    let (adapter, server) = servers.into_iter().next().unwrap();
+    assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
+    assert_eq!(server.server_id(), LanguageServerId(0));
+    // `workspace_folders` are set to the rooting point.
+    assert_eq!(
+        server.workspace_folders(),
+        BTreeSet::from_iter(
+            [Url::from_file_path(path!("/the-root/project-a")).unwrap()].into_iter()
+        )
+    );
+
+    let (second_project_buffer, _other_handle) = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer_with_lsp(path!("/the-root/project-b/source_file.py"), cx)
+        })
+        .await
+        .unwrap();
+    cx.executor().run_until_parked();
+    let servers = project.update(cx, |project, cx| {
+        project.lsp_store.update(cx, |this, cx| {
+            second_project_buffer.update(cx, |buffer, cx| {
+                this.language_servers_for_local_buffer(buffer, cx)
+                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
+                    .collect::<Vec<_>>()
+            })
+        })
+    });
+    cx.executor().run_until_parked();
+    assert_eq!(servers.len(), 1);
+    let (adapter, server) = servers.into_iter().next().unwrap();
+    assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
+    // We're not using venvs at all here, so both folders should fall under the same root.
+    assert_eq!(server.server_id(), LanguageServerId(0));
+    // Now, let's select a different toolchain for one of subprojects.
+    let (available_toolchains_for_b, root_path) = project
+        .update(cx, |this, cx| {
+            let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
+            this.available_toolchains(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from("project-b/source_file.py".as_ref()),
+                },
+                LanguageName::new("Python"),
+                cx,
+            )
+        })
+        .await
+        .expect("A toolchain to be discovered");
+    assert_eq!(root_path.as_ref(), Path::new("project-b"));
+    assert_eq!(available_toolchains_for_b.toolchains().len(), 1);
+    let currently_active_toolchain = project
+        .update(cx, |this, cx| {
+            let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
+            this.active_toolchain(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from("project-b/source_file.py".as_ref()),
+                },
+                LanguageName::new("Python"),
+                cx,
+            )
+        })
+        .await;
+
+    assert!(currently_active_toolchain.is_none());
+    let _ = project
+        .update(cx, |this, cx| {
+            let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
+            this.activate_toolchain(
+                ProjectPath {
+                    worktree_id,
+                    path: root_path,
+                },
+                available_toolchains_for_b
+                    .toolchains
+                    .into_iter()
+                    .next()
+                    .unwrap(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+    cx.run_until_parked();
+    let servers = project.update(cx, |project, cx| {
+        project.lsp_store.update(cx, |this, cx| {
+            second_project_buffer.update(cx, |buffer, cx| {
+                this.language_servers_for_local_buffer(buffer, cx)
+                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
+                    .collect::<Vec<_>>()
+            })
+        })
+    });
+    cx.executor().run_until_parked();
+    assert_eq!(servers.len(), 1);
+    let (adapter, server) = servers.into_iter().next().unwrap();
+    assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
+    // There's a new language server in town.
+    assert_eq!(server.server_id(), LanguageServerId(1));
+}
+
 #[gpui::test]
 async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
     init_test(cx);
@@ -8982,6 +9181,65 @@ fn rust_lang() -> Arc<Language> {
     ))
 }
 
+fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
+    struct PythonMootToolchainLister(Arc<FakeFs>);
+    #[async_trait]
+    impl ToolchainLister for PythonMootToolchainLister {
+        async fn list(
+            &self,
+            worktree_root: PathBuf,
+            subroot_relative_path: Option<Arc<Path>>,
+            _: Option<HashMap<String, String>>,
+        ) -> ToolchainList {
+            // This lister will always return a path .venv directories within ancestors
+            let ancestors = subroot_relative_path
+                .into_iter()
+                .flat_map(|path| path.ancestors().map(ToOwned::to_owned).collect::<Vec<_>>());
+            let mut toolchains = vec![];
+            for ancestor in ancestors {
+                let venv_path = worktree_root.join(ancestor).join(".venv");
+                if self.0.is_dir(&venv_path).await {
+                    toolchains.push(Toolchain {
+                        name: SharedString::new("Python Venv"),
+                        path: venv_path.to_string_lossy().into_owned().into(),
+                        language_name: LanguageName(SharedString::new_static("Python")),
+                        as_json: serde_json::Value::Null,
+                    })
+                }
+            }
+            ToolchainList {
+                toolchains,
+                ..Default::default()
+            }
+        }
+        // Returns a term which we should use in UI to refer to a toolchain.
+        fn term(&self) -> SharedString {
+            SharedString::new_static("virtual environment")
+        }
+        /// Returns the name of the manifest file for this toolchain.
+        fn manifest_name(&self) -> ManifestName {
+            SharedString::new_static("pyproject.toml").into()
+        }
+    }
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Python".into(),
+                matcher: LanguageMatcher {
+                    path_suffixes: vec!["py".to_string()],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            None, // We're not testing Python parsing with this language.
+        )
+        .with_manifest(Some(ManifestName::from(SharedString::new_static(
+            "pyproject.toml",
+        ))))
+        .with_toolchain_lister(Some(Arc::new(PythonMootToolchainLister(fs)))),
+    )
+}
+
 fn typescript_lang() -> Arc<Language> {
     Arc::new(Language::new(
         LanguageConfig {