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

Closes #ISSUE

Release Notes:

- N/A
This commit is contained in:
Piotr Osiewicz 2025-08-21 17:52:17 +02:00 committed by GitHub
parent 4bee06e507
commit 132daef9f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 262 additions and 4 deletions

View file

@ -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;

View file

@ -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

View file

@ -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 {