Hi! As part of https://github.com/zed-extensions/ruby/issues/162 we would like to rename HTML/ERB to HTML+ERB since it is more syntactically correct to treat such language as ERB on top of HTML rather than HTML or ERB. To keep the user experience intact, we outlined the prerequisites in the linked issue. This is the first PR that adds the HTML+ERB language name to the list of enabled languages for the Emmet extension. We will do the same for the Tailwind configuration in the Zed codebase. Once the new versions of Emmet and Zed are released, we will merge the pull request in the Ruby extension repository and release the updated version. After that, we will remove the old HTML/ERB and YAML/ERB languages. Let me know if that sounds good. Thanks! Release Notes: - N/A Co-authored-by: Marshall Bowers <git@maxdeviant.com>
223 lines
6.9 KiB
Rust
223 lines
6.9 KiB
Rust
use anyhow::{Context as _, Result};
|
|
use async_trait::async_trait;
|
|
use collections::HashMap;
|
|
use futures::StreamExt;
|
|
use gpui::AsyncApp;
|
|
use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
|
|
use lsp::{LanguageServerBinary, LanguageServerName};
|
|
use node_runtime::{NodeRuntime, VersionStrategy};
|
|
use project::{Fs, lsp_store::language_server_settings};
|
|
use serde_json::{Value, json};
|
|
use smol::fs;
|
|
use std::{
|
|
any::Any,
|
|
ffi::OsString,
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
use util::{ResultExt, maybe};
|
|
|
|
#[cfg(target_os = "windows")]
|
|
const SERVER_PATH: &str =
|
|
"node_modules/@tailwindcss/language-server/bin/tailwindcss-language-server";
|
|
#[cfg(not(target_os = "windows"))]
|
|
const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
|
|
|
|
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
|
vec![server_path.into(), "--stdio".into()]
|
|
}
|
|
|
|
pub struct TailwindLspAdapter {
|
|
node: NodeRuntime,
|
|
}
|
|
|
|
impl TailwindLspAdapter {
|
|
const SERVER_NAME: LanguageServerName =
|
|
LanguageServerName::new_static("tailwindcss-language-server");
|
|
const PACKAGE_NAME: &str = "@tailwindcss/language-server";
|
|
|
|
pub fn new(node: NodeRuntime) -> Self {
|
|
TailwindLspAdapter { node }
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspAdapter for TailwindLspAdapter {
|
|
fn name(&self) -> LanguageServerName {
|
|
Self::SERVER_NAME
|
|
}
|
|
|
|
async fn check_if_user_installed(
|
|
&self,
|
|
delegate: &dyn LspAdapterDelegate,
|
|
_: Option<Toolchain>,
|
|
_: &AsyncApp,
|
|
) -> Option<LanguageServerBinary> {
|
|
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
|
let env = delegate.shell_env().await;
|
|
|
|
Some(LanguageServerBinary {
|
|
path,
|
|
env: Some(env),
|
|
arguments: vec!["--stdio".into()],
|
|
})
|
|
}
|
|
|
|
async fn fetch_latest_server_version(
|
|
&self,
|
|
_: &dyn LspAdapterDelegate,
|
|
) -> Result<Box<dyn 'static + Any + Send>> {
|
|
Ok(Box::new(
|
|
self.node
|
|
.npm_package_latest_version(Self::PACKAGE_NAME)
|
|
.await?,
|
|
) as Box<_>)
|
|
}
|
|
|
|
async fn fetch_server_binary(
|
|
&self,
|
|
latest_version: Box<dyn 'static + Send + Any>,
|
|
container_dir: PathBuf,
|
|
_: &dyn LspAdapterDelegate,
|
|
) -> Result<LanguageServerBinary> {
|
|
let latest_version = latest_version.downcast::<String>().unwrap();
|
|
let server_path = container_dir.join(SERVER_PATH);
|
|
|
|
self.node
|
|
.npm_install_packages(
|
|
&container_dir,
|
|
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
|
)
|
|
.await?;
|
|
|
|
Ok(LanguageServerBinary {
|
|
path: self.node.binary_path().await?,
|
|
env: None,
|
|
arguments: server_binary_arguments(&server_path),
|
|
})
|
|
}
|
|
|
|
async fn check_if_version_installed(
|
|
&self,
|
|
version: &(dyn 'static + Send + Any),
|
|
container_dir: &PathBuf,
|
|
_: &dyn LspAdapterDelegate,
|
|
) -> Option<LanguageServerBinary> {
|
|
let version = version.downcast_ref::<String>().unwrap();
|
|
let server_path = container_dir.join(SERVER_PATH);
|
|
|
|
let should_install_language_server = self
|
|
.node
|
|
.should_install_npm_package(
|
|
Self::PACKAGE_NAME,
|
|
&server_path,
|
|
container_dir,
|
|
VersionStrategy::Latest(version),
|
|
)
|
|
.await;
|
|
|
|
if should_install_language_server {
|
|
None
|
|
} else {
|
|
Some(LanguageServerBinary {
|
|
path: self.node.binary_path().await.ok()?,
|
|
env: None,
|
|
arguments: server_binary_arguments(&server_path),
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn cached_server_binary(
|
|
&self,
|
|
container_dir: PathBuf,
|
|
_: &dyn LspAdapterDelegate,
|
|
) -> Option<LanguageServerBinary> {
|
|
get_cached_server_binary(container_dir, &self.node).await
|
|
}
|
|
|
|
async fn initialization_options(
|
|
self: Arc<Self>,
|
|
_: &dyn Fs,
|
|
_: &Arc<dyn LspAdapterDelegate>,
|
|
) -> Result<Option<serde_json::Value>> {
|
|
Ok(Some(json!({
|
|
"provideFormatter": true,
|
|
"userLanguages": {
|
|
"html": "html",
|
|
"css": "css",
|
|
"javascript": "javascript",
|
|
"typescriptreact": "typescriptreact",
|
|
},
|
|
})))
|
|
}
|
|
|
|
async fn workspace_configuration(
|
|
self: Arc<Self>,
|
|
_: &dyn Fs,
|
|
delegate: &Arc<dyn LspAdapterDelegate>,
|
|
_: Option<Toolchain>,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<Value> {
|
|
let mut tailwind_user_settings = cx.update(|cx| {
|
|
language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
|
|
.and_then(|s| s.settings.clone())
|
|
.unwrap_or_default()
|
|
})?;
|
|
|
|
if tailwind_user_settings.get("emmetCompletions").is_none() {
|
|
tailwind_user_settings["emmetCompletions"] = Value::Bool(true);
|
|
}
|
|
|
|
Ok(json!({
|
|
"tailwindCSS": tailwind_user_settings,
|
|
}))
|
|
}
|
|
|
|
fn language_ids(&self) -> HashMap<LanguageName, String> {
|
|
HashMap::from_iter([
|
|
(LanguageName::new("Astro"), "astro".to_string()),
|
|
(LanguageName::new("HTML"), "html".to_string()),
|
|
(LanguageName::new("CSS"), "css".to_string()),
|
|
(LanguageName::new("JavaScript"), "javascript".to_string()),
|
|
(LanguageName::new("TSX"), "typescriptreact".to_string()),
|
|
(LanguageName::new("Svelte"), "svelte".to_string()),
|
|
(LanguageName::new("Elixir"), "phoenix-heex".to_string()),
|
|
(LanguageName::new("HEEX"), "phoenix-heex".to_string()),
|
|
(LanguageName::new("ERB"), "erb".to_string()),
|
|
(LanguageName::new("HTML+ERB"), "erb".to_string()),
|
|
(LanguageName::new("HTML/ERB"), "erb".to_string()),
|
|
(LanguageName::new("PHP"), "php".to_string()),
|
|
(LanguageName::new("Vue.js"), "vue".to_string()),
|
|
])
|
|
}
|
|
}
|
|
|
|
async fn get_cached_server_binary(
|
|
container_dir: PathBuf,
|
|
node: &NodeRuntime,
|
|
) -> Option<LanguageServerBinary> {
|
|
maybe!(async {
|
|
let mut last_version_dir = None;
|
|
let mut entries = fs::read_dir(&container_dir).await?;
|
|
while let Some(entry) = entries.next().await {
|
|
let entry = entry?;
|
|
if entry.file_type().await?.is_dir() {
|
|
last_version_dir = Some(entry.path());
|
|
}
|
|
}
|
|
let last_version_dir = last_version_dir.context("no cached binary")?;
|
|
let server_path = last_version_dir.join(SERVER_PATH);
|
|
anyhow::ensure!(
|
|
server_path.exists(),
|
|
"missing executable in directory {last_version_dir:?}"
|
|
);
|
|
Ok(LanguageServerBinary {
|
|
path: node.binary_path().await?,
|
|
env: None,
|
|
arguments: server_binary_arguments(&server_path),
|
|
})
|
|
})
|
|
.await
|
|
.log_err()
|
|
}
|