ZIm/crates/zed/src/languages/php.rs
claytonrcarter cd4d2f7900
Add Prettier support for Vue, Markdown and PHP (#7904)
Current prettier support w/i Zed leaves out a few languages that are
officially supported by prettier. In particular, Vue and Markdown are
supported by the core prettier project, and PHP is supported via an
official plugin. I didn't see any open issues for this, but I have been
wondering for months why `"formatter": "prettier"` wasn't working on my
PHP files. Now that Zed is open source, I was able to find out why, and
fix it. 😄

I have been using this with PHP files daily for a week+ now, and I have
also used it successfully with Vue and Markdown files, though not as
extensively. I looked around and did not see any tests for specific
prettier language integrations, but if I missed them please let me know
and I'll add some tests.

**Notes**
- I did not add support for Ruby (which has an official prettier plugin)
because it seems to require some external dependencies (notably, Rudy
and some Gems). When those are present on the system and `$PATH`,
prettier will will work just fine on Ruby files if the plugin is set up
similar to how the PHP plugin is set up (I tried it), and I can add that
in here, if desired. The PHP plugin is pure JS (as I recall) and doesn't
have this issue.
- I did *not* add support for languages that have "community" plugins,
though I do note that Zed already ships with prettier support for svelte
enabled, which – if I understand correctly – is powered by a community
plugin. If desired, I could look at adding support/configuration to
enable prettier support for things like elm, erb, glsl, bash, toml.
Bash, in particular, *I* would find useful. 😄

Release Notes:

- Added prettier support for Vue, Markdown and PHP
2024-02-17 11:35:31 +02:00

140 lines
4 KiB
Rust

use anyhow::{anyhow, Result};
use async_trait::async_trait;
use collections::HashMap;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use smol::{fs, stream::StreamExt};
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
fn intelephense_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
pub struct IntelephenseVersion(String);
pub struct IntelephenseLspAdapter {
node: Arc<dyn NodeRuntime>,
}
impl IntelephenseLspAdapter {
const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
Self { node }
}
}
#[async_trait]
impl LspAdapter for IntelephenseLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("intelephense".into())
}
fn short_name(&self) -> &'static str {
"php"
}
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(IntelephenseVersion(
self.node.npm_package_latest_version("intelephense").await?,
)) as Box<_>)
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<IntelephenseVersion>().unwrap();
let server_path = container_dir.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
self.node
.npm_install_packages(&container_dir, &[("intelephense", version.0.as_str())])
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
arguments: intelephense_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 installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &*self.node).await
}
async fn label_for_completion(
&self,
_item: &lsp::CompletionItem,
_language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
None
}
fn initialization_options(&self) -> Option<serde_json::Value> {
None
}
fn language_ids(&self) -> HashMap<String, String> {
HashMap::from_iter([("PHP".into(), "php".into())])
}
fn prettier_plugins(&self) -> &[&'static str] {
&["@prettier/plugin-php"]
}
}
async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
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.ok_or_else(|| anyhow!("no cached binary"))?;
let server_path = last_version_dir.join(IntelephenseLspAdapter::SERVER_PATH);
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
arguments: intelephense_server_binary_arguments(&server_path),
})
} else {
Err(anyhow!(
"missing executable in directory {:?}",
last_version_dir
))
}
})
.await
.log_err()
}