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 // We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet.
52 vue_version: "1.8".to_string(),
53 ts_version: self.node.npm_package_latest_version("typescript").await?,
54 }) as Box<_>)
55 }
56 async fn initialization_options(
57 self: Arc<Self>,
58 _: &Arc<dyn LspAdapterDelegate>,
59 ) -> Result<Option<serde_json::Value>> {
60 let typescript_sdk_path = self.typescript_install_path.lock();
61 let typescript_sdk_path = typescript_sdk_path
62 .as_ref()
63 .expect("initialization_options called without a container_dir for typescript");
64
65 Ok(Some(serde_json::json!({
66 "typescript": {
67 "tsdk": typescript_sdk_path
68 }
69 })))
70 }
71 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
72 // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
73 // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
74 Some(vec![
75 CodeActionKind::EMPTY,
76 CodeActionKind::QUICKFIX,
77 CodeActionKind::REFACTOR_REWRITE,
78 ])
79 }
80 async fn fetch_server_binary(
81 &self,
82 latest_version: Box<dyn 'static + Send + Any>,
83 container_dir: PathBuf,
84 _: &dyn LspAdapterDelegate,
85 ) -> Result<LanguageServerBinary> {
86 let latest_version = latest_version.downcast::<VueLspVersion>().unwrap();
87 let server_path = container_dir.join(Self::SERVER_PATH);
88 let ts_path = container_dir.join(Self::TYPESCRIPT_PATH);
89
90 let vue_package_name = "@vue/language-server";
91 let should_install_vue_language_server = self
92 .node
93 .should_install_npm_package(
94 vue_package_name,
95 &server_path,
96 &container_dir,
97 &latest_version.vue_version,
98 )
99 .await;
100
101 if should_install_vue_language_server {
102 self.node
103 .npm_install_packages(
104 &container_dir,
105 &[(vue_package_name, latest_version.vue_version.as_str())],
106 )
107 .await?;
108 }
109 ensure!(
110 fs::metadata(&server_path).await.is_ok(),
111 "@vue/language-server package installation failed"
112 );
113
114 let ts_package_name = "typescript";
115 let should_install_ts_language_server = self
116 .node
117 .should_install_npm_package(
118 ts_package_name,
119 &server_path,
120 &container_dir,
121 &latest_version.ts_version,
122 )
123 .await;
124
125 if should_install_ts_language_server {
126 self.node
127 .npm_install_packages(
128 &container_dir,
129 &[(ts_package_name, latest_version.ts_version.as_str())],
130 )
131 .await?;
132 }
133
134 ensure!(
135 fs::metadata(&ts_path).await.is_ok(),
136 "typescript for Vue package installation failed"
137 );
138 *self.typescript_install_path.lock() = Some(ts_path);
139 Ok(LanguageServerBinary {
140 path: self.node.binary_path().await?,
141 env: None,
142 arguments: vue_server_binary_arguments(&server_path),
143 })
144 }
145
146 async fn cached_server_binary(
147 &self,
148 container_dir: PathBuf,
149 _: &dyn LspAdapterDelegate,
150 ) -> Option<LanguageServerBinary> {
151 let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
152 *self.typescript_install_path.lock() = Some(ts_path);
153 Some(server)
154 }
155
156 async fn installation_test_binary(
157 &self,
158 container_dir: PathBuf,
159 ) -> Option<LanguageServerBinary> {
160 let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
161 .await
162 .map(|(mut binary, ts_path)| {
163 binary.arguments = vec!["--help".into()];
164 (binary, ts_path)
165 })?;
166 *self.typescript_install_path.lock() = Some(ts_path);
167 Some(server)
168 }
169
170 async fn label_for_completion(
171 &self,
172 item: &lsp::CompletionItem,
173 language: &Arc<language::Language>,
174 ) -> Option<language::CodeLabel> {
175 use lsp::CompletionItemKind as Kind;
176 let len = item.label.len();
177 let grammar = language.grammar()?;
178 let highlight_id = match item.kind? {
179 Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
180 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
181 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
182 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
183 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
184 Kind::VARIABLE => grammar.highlight_id_for_name("type"),
185 Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
186 Kind::VALUE => grammar.highlight_id_for_name("tag"),
187 _ => None,
188 }?;
189
190 let text = match &item.detail {
191 Some(detail) => format!("{} {}", item.label, detail),
192 None => item.label.clone(),
193 };
194
195 Some(language::CodeLabel {
196 text,
197 runs: vec![(0..len, highlight_id)],
198 filter_range: 0..len,
199 })
200 }
201}
202
203fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
204 vec![server_path.into(), "--stdio".into()]
205}
206
207type TypescriptPath = PathBuf;
208async fn get_cached_server_binary(
209 container_dir: PathBuf,
210 node: Arc<dyn NodeRuntime>,
211) -> Option<(LanguageServerBinary, TypescriptPath)> {
212 maybe!(async {
213 let mut last_version_dir = None;
214 let mut entries = fs::read_dir(&container_dir).await?;
215 while let Some(entry) = entries.next().await {
216 let entry = entry?;
217 if entry.file_type().await?.is_dir() {
218 last_version_dir = Some(entry.path());
219 }
220 }
221 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
222 let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
223 let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
224 if server_path.exists() && typescript_path.exists() {
225 Ok((
226 LanguageServerBinary {
227 path: node.binary_path().await?,
228 env: None,
229 arguments: vue_server_binary_arguments(&server_path),
230 },
231 typescript_path,
232 ))
233 } else {
234 Err(anyhow!(
235 "missing executable in directory {:?}",
236 last_version_dir
237 ))
238 }
239 })
240 .await
241 .log_err()
242}