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}