lsp: Add basic test for server tree toolchain use (#36692)
Closes #ISSUE Release Notes: - N/A
This commit is contained in:
parent
4bee06e507
commit
132daef9f6
4 changed files with 262 additions and 4 deletions
|
@ -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>,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue