1use anyhow::{anyhow, ensure, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4pub use language::*;
5use lsp::{CodeActionKind, LanguageServerBinary};
6use node_runtime::NodeRuntime;
7use parking_lot::Mutex;
8use smol::fs::{self};
9use std::{
10 any::Any,
11 ffi::OsString,
12 path::{Path, PathBuf},
13 sync::Arc,
14};
15use util::{maybe, ResultExt};
16
17pub struct VueLspVersion {
18 vue_version: String,
19 ts_version: String,
20}
21
22pub struct VueLspAdapter {
23 node: Arc<dyn NodeRuntime>,
24 typescript_install_path: Mutex<Option<PathBuf>>,
25}
26
27impl VueLspAdapter {
28 const SERVER_PATH: &'static str =
29 "node_modules/@vue/language-server/bin/vue-language-server.js";
30 // TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options.
31 const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib";
32 pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
33 let typescript_install_path = Mutex::new(None);
34 Self {
35 node,
36 typescript_install_path,
37 }
38 }
39}
40#[async_trait(?Send)]
41impl super::LspAdapter for VueLspAdapter {
42 fn name(&self) -> LanguageServerName {
43 LanguageServerName("vue-language-server".into())
44 }
45
46 async fn fetch_latest_server_version(
47 &self,
48 _: &dyn LspAdapterDelegate,
49 ) -> Result<Box<dyn 'static + Send + Any>> {
50 Ok(Box::new(VueLspVersion {
51 vue_version: self
52 .node
53 .npm_package_latest_version("@vue/language-server")
54 .await?,
55 ts_version: self.node.npm_package_latest_version("typescript").await?,
56 }) as Box<_>)
57 }
58 async fn initialization_options(
59 self: Arc<Self>,
60 _: &Arc<dyn LspAdapterDelegate>,
61 ) -> Result<Option<serde_json::Value>> {
62 let typescript_sdk_path = self.typescript_install_path.lock();
63 let typescript_sdk_path = typescript_sdk_path
64 .as_ref()
65 .expect("initialization_options called without a container_dir for typescript");
66
67 Ok(Some(serde_json::json!({
68 "typescript": {
69 "tsdk": typescript_sdk_path
70 }
71 })))
72 }
73 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
74 // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
75 // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
76 Some(vec![
77 CodeActionKind::EMPTY,
78 CodeActionKind::QUICKFIX,
79 CodeActionKind::REFACTOR_REWRITE,
80 ])
81 }
82 async fn fetch_server_binary(
83 &self,
84 latest_version: Box<dyn 'static + Send + Any>,
85 container_dir: PathBuf,
86 _: &dyn LspAdapterDelegate,
87 ) -> Result<LanguageServerBinary> {
88 let latest_version = latest_version.downcast::<VueLspVersion>().unwrap();
89 let server_path = container_dir.join(Self::SERVER_PATH);
90 let ts_path = container_dir.join(Self::TYPESCRIPT_PATH);
91
92 let vue_package_name = "@vue/language-server";
93 let should_install_vue_language_server = self
94 .node
95 .should_install_npm_package(
96 vue_package_name,
97 &server_path,
98 &container_dir,
99 &latest_version.vue_version,
100 )
101 .await;
102
103 if should_install_vue_language_server {
104 self.node
105 .npm_install_packages(
106 &container_dir,
107 &[(vue_package_name, latest_version.vue_version.as_str())],
108 )
109 .await?;
110 }
111 ensure!(
112 fs::metadata(&server_path).await.is_ok(),
113 "@vue/language-server package installation failed"
114 );
115
116 let ts_package_name = "typescript";
117 let should_install_ts_language_server = self
118 .node
119 .should_install_npm_package(
120 ts_package_name,
121 &server_path,
122 &container_dir,
123 &latest_version.ts_version,
124 )
125 .await;
126
127 if should_install_ts_language_server {
128 self.node
129 .npm_install_packages(
130 &container_dir,
131 &[(ts_package_name, latest_version.ts_version.as_str())],
132 )
133 .await?;
134 }
135
136 ensure!(
137 fs::metadata(&ts_path).await.is_ok(),
138 "typescript for Vue package installation failed"
139 );
140 *self.typescript_install_path.lock() = Some(ts_path);
141 Ok(LanguageServerBinary {
142 path: self.node.binary_path().await?,
143 env: None,
144 arguments: vue_server_binary_arguments(&server_path),
145 })
146 }
147
148 async fn cached_server_binary(
149 &self,
150 container_dir: PathBuf,
151 _: &dyn LspAdapterDelegate,
152 ) -> Option<LanguageServerBinary> {
153 let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
154 *self.typescript_install_path.lock() = Some(ts_path);
155 Some(server)
156 }
157
158 async fn installation_test_binary(
159 &self,
160 container_dir: PathBuf,
161 ) -> Option<LanguageServerBinary> {
162 let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
163 .await
164 .map(|(mut binary, ts_path)| {
165 binary.arguments = vec!["--help".into()];
166 (binary, ts_path)
167 })?;
168 *self.typescript_install_path.lock() = Some(ts_path);
169 Some(server)
170 }
171
172 async fn label_for_completion(
173 &self,
174 item: &lsp::CompletionItem,
175 language: &Arc<language::Language>,
176 ) -> Option<language::CodeLabel> {
177 use lsp::CompletionItemKind as Kind;
178 let len = item.label.len();
179 let grammar = language.grammar()?;
180 let highlight_id = match item.kind? {
181 Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
182 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
183 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
184 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
185 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
186 Kind::VARIABLE => grammar.highlight_id_for_name("type"),
187 Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
188 Kind::VALUE => grammar.highlight_id_for_name("tag"),
189 _ => None,
190 }?;
191
192 let text = match &item.detail {
193 Some(detail) => format!("{} {}", item.label, detail),
194 None => item.label.clone(),
195 };
196
197 Some(language::CodeLabel {
198 text,
199 runs: vec![(0..len, highlight_id)],
200 filter_range: 0..len,
201 })
202 }
203}
204
205fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
206 vec![server_path.into(), "--stdio".into()]
207}
208
209type TypescriptPath = PathBuf;
210async fn get_cached_server_binary(
211 container_dir: PathBuf,
212 node: Arc<dyn NodeRuntime>,
213) -> Option<(LanguageServerBinary, TypescriptPath)> {
214 maybe!(async {
215 let mut last_version_dir = None;
216 let mut entries = fs::read_dir(&container_dir).await?;
217 while let Some(entry) = entries.next().await {
218 let entry = entry?;
219 if entry.file_type().await?.is_dir() {
220 last_version_dir = Some(entry.path());
221 }
222 }
223 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
224 let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
225 let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
226 if server_path.exists() && typescript_path.exists() {
227 Ok((
228 LanguageServerBinary {
229 path: node.binary_path().await?,
230 env: None,
231 arguments: vue_server_binary_arguments(&server_path),
232 },
233 typescript_path,
234 ))
235 } else {
236 Err(anyhow!(
237 "missing executable in directory {:?}",
238 last_version_dir
239 ))
240 }
241 })
242 .await
243 .log_err()
244}