ZIm/crates/languages/src/css.rs
Kirill Bulatov 91b0ca0895
Omit tsdk_path from the servers' options if it does not exist (#23525)
Part of https://github.com/zed-industries/zed/issues/22606

Before, `tsdk_path` for vtsls and typescript-language-server
unconditionally set a `tsdk`/`tsserver` property for the corresponding
language server, even if there were no such directory at all.
Instead, make the corresponding code to omit such property if it was not
found on the FS.

Release Notes:

- Fixed "The path /.../tsserver.js doesn't point to a valid tsserver
install. Falling back to bundled TypeScript version." pop-up appearing
2025-01-23 11:17:32 +00:00

206 lines
5.8 KiB
Rust

use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::StreamExt;
use language::{LspAdapter, LspAdapterDelegate};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::Fs;
use serde_json::json;
use smol::fs;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{maybe, ResultExt};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
pub struct CssLspAdapter {
node: NodeRuntime,
}
impl CssLspAdapter {
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
pub fn new(node: NodeRuntime) -> Self {
CssLspAdapter { node }
}
}
#[async_trait(?Send)]
impl LspAdapter for CssLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-css-language-server".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("vscode-langservers-extracted")
.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, &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
})))
}
}
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.ok_or_else(|| anyhow!("no cached binary"))?;
let server_path = last_version_dir.join(SERVER_PATH);
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {
Err(anyhow!(
"missing executable in directory {:?}",
last_version_dir
))
}
})
.await
.log_err()
}
#[cfg(test)]
mod tests {
use gpui::{Context, TestAppContext};
use unindent::Unindent;
#[gpui::test]
async fn test_outline(cx: &mut TestAppContext) {
let language = crate::language("css", tree_sitter_css::LANGUAGE.into());
let text = r#"
/* Import statement */
@import './fonts.css';
/* multiline list of selectors with nesting */
.test-class,
div {
.nested-class {
color: red;
}
}
/* descendant selectors */
.test .descendant {}
/* pseudo */
.test:not(:hover) {}
/* media queries */
@media screen and (min-width: 3000px) {
.desktop-class {}
}
"#
.unindent();
let buffer =
cx.new_model(|cx| language::Buffer::local(text, cx).with_language(language, cx));
let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
assert_eq!(
outline
.items
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[
("@import './fonts.css'", 0),
(".test-class, div", 0),
(".nested-class", 1),
(".test .descendant", 0),
(".test:not(:hover)", 0),
("@media screen and (min-width: 3000px)", 0),
(".desktop-class", 1),
]
);
}
}