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