css.rs

  1use anyhow::{anyhow, Result};
  2use async_trait::async_trait;
  3use futures::StreamExt;
  4use language::{LspAdapter, LspAdapterDelegate};
  5use lsp::{LanguageServerBinary, LanguageServerName};
  6use node_runtime::NodeRuntime;
  7use serde_json::json;
  8use smol::fs;
  9use std::{
 10    any::Any,
 11    ffi::OsString,
 12    path::{Path, PathBuf},
 13    sync::Arc,
 14};
 15use util::{maybe, ResultExt};
 16
 17const SERVER_PATH: &str =
 18    "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
 19
 20fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 21    vec![server_path.into(), "--stdio".into()]
 22}
 23
 24pub struct CssLspAdapter {
 25    node: NodeRuntime,
 26}
 27
 28impl CssLspAdapter {
 29    const PACKAGE_NAME: &str = "vscode-langservers-extracted";
 30    pub fn new(node: NodeRuntime) -> Self {
 31        CssLspAdapter { node }
 32    }
 33}
 34
 35#[async_trait(?Send)]
 36impl LspAdapter for CssLspAdapter {
 37    fn name(&self) -> LanguageServerName {
 38        LanguageServerName("vscode-css-language-server".into())
 39    }
 40
 41    async fn fetch_latest_server_version(
 42        &self,
 43        _: &dyn LspAdapterDelegate,
 44    ) -> Result<Box<dyn 'static + Any + Send>> {
 45        Ok(Box::new(
 46            self.node
 47                .npm_package_latest_version("vscode-langservers-extracted")
 48                .await?,
 49        ) as Box<_>)
 50    }
 51
 52    async fn fetch_server_binary(
 53        &self,
 54        latest_version: Box<dyn 'static + Send + Any>,
 55        container_dir: PathBuf,
 56        _: &dyn LspAdapterDelegate,
 57    ) -> Result<LanguageServerBinary> {
 58        let latest_version = latest_version.downcast::<String>().unwrap();
 59        let server_path = container_dir.join(SERVER_PATH);
 60
 61        self.node
 62            .npm_install_packages(
 63                &container_dir,
 64                &[(Self::PACKAGE_NAME, latest_version.as_str())],
 65            )
 66            .await?;
 67
 68        Ok(LanguageServerBinary {
 69            path: self.node.binary_path().await?,
 70            env: None,
 71            arguments: server_binary_arguments(&server_path),
 72        })
 73    }
 74
 75    async fn check_if_version_installed(
 76        &self,
 77        version: &(dyn 'static + Send + Any),
 78        container_dir: &PathBuf,
 79        _: &dyn LspAdapterDelegate,
 80    ) -> Option<LanguageServerBinary> {
 81        let version = version.downcast_ref::<String>().unwrap();
 82        let server_path = container_dir.join(SERVER_PATH);
 83
 84        let should_install_language_server = self
 85            .node
 86            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
 87            .await;
 88
 89        if should_install_language_server {
 90            None
 91        } else {
 92            Some(LanguageServerBinary {
 93                path: self.node.binary_path().await.ok()?,
 94                env: None,
 95                arguments: server_binary_arguments(&server_path),
 96            })
 97        }
 98    }
 99
100    async fn cached_server_binary(
101        &self,
102        container_dir: PathBuf,
103        _: &dyn LspAdapterDelegate,
104    ) -> Option<LanguageServerBinary> {
105        get_cached_server_binary(container_dir, &self.node).await
106    }
107
108    async fn initialization_options(
109        self: Arc<Self>,
110        _: &Arc<dyn LspAdapterDelegate>,
111    ) -> Result<Option<serde_json::Value>> {
112        Ok(Some(json!({
113            "provideFormatter": true
114        })))
115    }
116}
117
118async fn get_cached_server_binary(
119    container_dir: PathBuf,
120    node: &NodeRuntime,
121) -> Option<LanguageServerBinary> {
122    maybe!(async {
123        let mut last_version_dir = None;
124        let mut entries = fs::read_dir(&container_dir).await?;
125        while let Some(entry) = entries.next().await {
126            let entry = entry?;
127            if entry.file_type().await?.is_dir() {
128                last_version_dir = Some(entry.path());
129            }
130        }
131        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
132        let server_path = last_version_dir.join(SERVER_PATH);
133        if server_path.exists() {
134            Ok(LanguageServerBinary {
135                path: node.binary_path().await?,
136                env: None,
137                arguments: server_binary_arguments(&server_path),
138            })
139        } else {
140            Err(anyhow!(
141                "missing executable in directory {:?}",
142                last_version_dir
143            ))
144        }
145    })
146    .await
147    .log_err()
148}
149
150#[cfg(test)]
151mod tests {
152    use gpui::{Context, TestAppContext};
153    use unindent::Unindent;
154
155    #[gpui::test]
156    async fn test_outline(cx: &mut TestAppContext) {
157        let language = crate::language("css", tree_sitter_css::LANGUAGE.into());
158
159        let text = r#"
160            /* Import statement */
161            @import './fonts.css';
162
163            /* multiline list of selectors with nesting */
164            .test-class,
165            div {
166                .nested-class {
167                    color: red;
168                }
169            }
170
171            /* descendant selectors */
172            .test .descendant {}
173
174            /* pseudo */
175            .test:not(:hover) {}
176
177            /* media queries */
178            @media screen and (min-width: 3000px) {
179                .desktop-class {}
180            }
181        "#
182        .unindent();
183
184        let buffer =
185            cx.new_model(|cx| language::Buffer::local(text, cx).with_language(language, cx));
186        let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
187        assert_eq!(
188            outline
189                .items
190                .iter()
191                .map(|item| (item.text.as_str(), item.depth))
192                .collect::<Vec<_>>(),
193            &[
194                ("@import './fonts.css'", 0),
195                (".test-class, div", 0),
196                (".nested-class", 1),
197                (".test .descendant", 0),
198                (".test:not(:hover)", 0),
199                ("@media screen and (min-width: 3000px)", 0),
200                (".desktop-class", 1),
201            ]
202        );
203    }
204}