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;
|
type DefaultIndex = usize;
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct ToolchainList {
|
pub struct ToolchainList {
|
||||||
pub toolchains: Vec<Toolchain>,
|
pub toolchains: Vec<Toolchain>,
|
||||||
pub default: Option<DefaultIndex>,
|
pub default: Option<DefaultIndex>,
|
||||||
|
|
|
@ -4643,7 +4643,6 @@ impl LspStore {
|
||||||
Some((file, language, raw_buffer.remote_id()))
|
Some((file, language, raw_buffer.remote_id()))
|
||||||
})
|
})
|
||||||
.sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible()));
|
.sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible()));
|
||||||
|
|
||||||
for (file, language, buffer_id) in buffers {
|
for (file, language, buffer_id) in buffers {
|
||||||
let worktree_id = file.worktree_id(cx);
|
let worktree_id = file.worktree_id(cx);
|
||||||
let Some(worktree) = local
|
let Some(worktree) = local
|
||||||
|
@ -4685,7 +4684,6 @@ impl LspStore {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
let server_id = node.server_id_or_init(|disposition| {
|
let server_id = node.server_id_or_init(|disposition| {
|
||||||
let path = &disposition.path;
|
let path = &disposition.path;
|
||||||
|
|
|
@ -181,6 +181,7 @@ impl LanguageServerTree {
|
||||||
&root_path.path,
|
&root_path.path,
|
||||||
language_name.clone(),
|
language_name.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
(
|
(
|
||||||
Arc::new(InnerTreeNode::new(
|
Arc::new(InnerTreeNode::new(
|
||||||
adapter.name(),
|
adapter.name(),
|
||||||
|
@ -408,6 +409,7 @@ impl ServerTreeRebase {
|
||||||
if live_node.id.get().is_some() {
|
if live_node.id.get().is_some() {
|
||||||
return Some(node);
|
return Some(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
let disposition = &live_node.disposition;
|
let disposition = &live_node.disposition;
|
||||||
let Some((existing_node, _)) = self
|
let Some((existing_node, _)) = self
|
||||||
.old_contents
|
.old_contents
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::{
|
||||||
Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
|
Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
|
||||||
*,
|
*,
|
||||||
};
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
use buffer_diff::{
|
use buffer_diff::{
|
||||||
BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
|
BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
|
||||||
DiffHunkStatusKind, assert_hunks,
|
DiffHunkStatusKind, assert_hunks,
|
||||||
|
@ -21,7 +22,8 @@ use http_client::Url;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter,
|
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},
|
language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
|
||||||
tree_sitter_rust, tree_sitter_typescript,
|
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]
|
#[gpui::test]
|
||||||
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
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> {
|
fn typescript_lang() -> Arc<Language> {
|
||||||
Arc::new(Language::new(
|
Arc::new(Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue