Add initial support for defining language server adapters in WebAssembly-based extensions (#8645)

This PR adds **internal** ability to run arbitrary language servers via
WebAssembly extensions. The functionality isn't exposed yet - we're just
landing this in this early state because there have been a lot of
changes to the `LspAdapter` trait, and other language server logic.

## Next steps

* Currently, wasm extensions can only define how to *install* and run a
language server, they can't yet implement the other LSP adapter methods,
such as formatting completion labels and workspace symbols.
* We don't have an automatic way to install or develop these types of
extensions
* We don't have a way to package these types of extensions in our
extensions repo, to make them available via our extensions API.
* The Rust extension API crate, `zed-extension-api` has not yet been
published to crates.io, because we still consider the API a work in
progress.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Max Brunsfeld 2024-03-01 16:00:55 -08:00 committed by GitHub
parent f3f2225a8e
commit 268fa1cbaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 3714 additions and 1973 deletions

View file

@ -10,6 +10,7 @@ pub mod terminals;
mod project_tests;
use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
@ -847,10 +848,12 @@ impl Project {
let current_lsp_settings = &self.current_lsp_settings;
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
let language = languages.iter().find_map(|l| {
let adapter = l
.lsp_adapters()
let adapter = self
.languages
.lsp_adapters(l)
.iter()
.find(|adapter| &adapter.name == started_lsp_name)?;
.find(|adapter| &adapter.name == started_lsp_name)?
.clone();
Some((l, adapter))
});
if let Some((language, adapter)) = language {
@ -889,9 +892,11 @@ impl Project {
let mut prettier_plugins_by_worktree = HashMap::default();
for (worktree, language, settings) in language_formatters_to_check {
if let Some(plugins) =
prettier_support::prettier_plugins_for_language(&language, &settings)
{
if let Some(plugins) = prettier_support::prettier_plugins_for_language(
&self.languages,
&language,
&settings,
) {
prettier_plugins_by_worktree
.entry(worktree)
.or_insert_with(|| HashSet::default())
@ -2047,7 +2052,7 @@ impl Project {
}
if let Some(language) = language {
for adapter in language.lsp_adapters() {
for adapter in self.languages.lsp_adapters(&language) {
let language_id = adapter.language_ids.get(language.name().as_ref()).cloned();
let server = self
.language_server_ids
@ -2118,10 +2123,12 @@ impl Project {
let worktree_id = old_file.worktree_id(cx);
let ids = &self.language_server_ids;
let language = buffer.language().cloned();
let adapters = language.iter().flat_map(|language| language.lsp_adapters());
for &server_id in adapters.flat_map(|a| ids.get(&(worktree_id, a.name.clone()))) {
buffer.update_diagnostics(server_id, Default::default(), cx);
if let Some(language) = buffer.language().cloned() {
for adapter in self.languages.lsp_adapters(&language) {
if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) {
buffer.update_diagnostics(*server_id, Default::default(), cx);
}
}
}
self.buffer_snapshots.remove(&buffer.remote_id());
@ -2701,9 +2708,11 @@ impl Project {
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
if let Some(prettier_plugins) =
prettier_support::prettier_plugins_for_language(&new_language, &settings)
{
if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language(
&self.languages,
&new_language,
&settings,
) {
self.install_default_prettier(worktree, prettier_plugins, cx);
};
if let Some(file) = buffer_file {
@ -2726,7 +2735,7 @@ impl Project {
return;
}
for adapter in language.lsp_adapters() {
for adapter in self.languages.clone().lsp_adapters(&language) {
self.start_language_server(worktree, adapter.clone(), language.clone(), cx);
}
}
@ -3240,7 +3249,11 @@ impl Project {
};
if file.worktree.read(cx).id() != key.0
|| !language.lsp_adapters().iter().any(|a| a.name == key.1)
|| !self
.languages
.lsp_adapters(&language)
.iter()
.any(|a| a.name == key.1)
{
continue;
}
@ -3433,8 +3446,10 @@ impl Project {
) {
let worktree_id = worktree.read(cx).id();
let stop_tasks = language
.lsp_adapters()
let stop_tasks = self
.languages
.clone()
.lsp_adapters(&language)
.iter()
.map(|adapter| {
let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx);
@ -4785,14 +4800,15 @@ impl Project {
.languages
.language_for_file(&project_path.path, None)
.unwrap_or_else(move |_| adapter_language);
let language_server_name = adapter.name.clone();
let adapter = adapter.clone();
Some(async move {
let language = language.await;
let label =
language.label_for_symbol(&symbol_name, symbol_kind).await;
let label = adapter
.label_for_symbol(&symbol_name, symbol_kind, &language)
.await;
Symbol {
language_server_name,
language_server_name: adapter.name.clone(),
source_worktree_id,
path: project_path,
label: label.unwrap_or_else(|| {
@ -7972,6 +7988,7 @@ impl Project {
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::ApplyCompletionAdditionalEditsResponse> {
let languages = this.update(&mut cx, |this, _| this.languages.clone())?;
let (buffer, completion) = this.update(&mut cx, |this, cx| {
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let buffer = this
@ -7986,6 +8003,7 @@ impl Project {
.completion
.ok_or_else(|| anyhow!("invalid completion"))?,
language.cloned(),
&languages,
);
Ok::<_, anyhow::Error>((buffer, completion))
})??;
@ -8713,6 +8731,9 @@ impl Project {
.language_for_file(&path.path, None)
.await
.log_err();
let adapter = language
.as_ref()
.and_then(|language| languages.lsp_adapters(language).first().cloned());
Ok(Symbol {
language_server_name: LanguageServerName(
serialized_symbol.language_server_name.into(),
@ -8720,10 +8741,10 @@ impl Project {
source_worktree_id,
path,
label: {
match language {
Some(language) => {
language
.label_for_symbol(&serialized_symbol.name, kind)
match language.as_ref().zip(adapter.as_ref()) {
Some((language, adapter)) => {
adapter
.label_for_symbol(&serialized_symbol.name, kind, language)
.await
}
None => None,
@ -8975,6 +8996,17 @@ impl Project {
self.supplementary_language_servers.iter()
}
pub fn language_server_adapter_for_id(
&self,
id: LanguageServerId,
) -> Option<Arc<CachedLspAdapter>> {
if let Some(LanguageServerState::Running { adapter, .. }) = self.language_servers.get(&id) {
Some(adapter.clone())
} else {
None
}
}
pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
Some(server.clone())
@ -9025,8 +9057,8 @@ impl Project {
) -> Vec<LanguageServerId> {
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
let worktree_id = file.worktree_id(cx);
language
.lsp_adapters()
self.languages
.lsp_adapters(&language)
.iter()
.flat_map(|adapter| {
let key = (worktree_id, adapter.name.clone());
@ -9190,20 +9222,25 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
struct ProjectLspAdapterDelegate {
project: Model<Project>,
worktree: Model<Worktree>,
worktree: worktree::Snapshot,
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
}
impl ProjectLspAdapterDelegate {
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
Arc::new(Self {
project: cx.handle(),
worktree: worktree.clone(),
worktree: worktree.read(cx).snapshot(),
fs: project.fs.clone(),
http_client: project.client.http_client(),
language_registry: project.languages.clone(),
})
}
}
#[async_trait]
impl LspAdapterDelegate for ProjectLspAdapterDelegate {
fn show_notification(&self, message: &str, cx: &mut AppContext) {
self.project
@ -9214,41 +9251,50 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
self.http_client.clone()
}
fn which_command(
&self,
command: OsString,
cx: &AppContext,
) -> Task<Option<(PathBuf, HashMap<String, String>)>> {
let worktree_abs_path = self.worktree.read(cx).abs_path();
let command = command.to_owned();
async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap<String, String>)> {
let worktree_abs_path = self.worktree.abs_path();
cx.background_executor().spawn(async move {
let shell_env = load_shell_environment(&worktree_abs_path)
.await
.with_context(|| {
format!(
"failed to determine load login shell environment in {worktree_abs_path:?}"
)
})
.log_err();
let shell_env = load_shell_environment(&worktree_abs_path)
.await
.with_context(|| {
format!("failed to determine load login shell environment in {worktree_abs_path:?}")
})
.log_err();
if let Some(shell_env) = shell_env {
let shell_path = shell_env.get("PATH");
match which::which_in(&command, shell_path, &worktree_abs_path) {
Ok(command_path) => Some((command_path, shell_env)),
Err(error) => {
log::warn!(
"failed to determine path for command {:?} in shell PATH {:?}: {error}",
command.to_string_lossy(),
shell_path.map(String::as_str).unwrap_or("")
);
None
}
if let Some(shell_env) = shell_env {
let shell_path = shell_env.get("PATH");
match which::which_in(&command, shell_path, &worktree_abs_path) {
Ok(command_path) => Some((command_path, shell_env)),
Err(error) => {
log::warn!(
"failed to determine path for command {:?} in shell PATH {:?}: {error}",
command.to_string_lossy(),
shell_path.map(String::as_str).unwrap_or("")
);
None
}
} else {
None
}
})
} else {
None
}
}
fn update_status(
&self,
server_name: LanguageServerName,
status: language::LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(server_name, status);
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
if self.worktree.entry_for_path(&path).is_none() {
return Err(anyhow!("no such path {path:?}"));
}
let path = self.worktree.absolutize(path.as_ref())?;
let content = self.fs.load(&path).await?;
Ok(content)
}
}