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::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 async 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 async 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 arguments: vue_server_binary_arguments(&server_path),
122 })
123 }
124
125 async fn cached_server_binary(
126 &self,
127 container_dir: PathBuf,
128 _: &dyn LspAdapterDelegate,
129 ) -> Option<LanguageServerBinary> {
130 let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
131 *self.typescript_install_path.lock() = Some(ts_path);
132 Some(server)
133 }
134
135 async fn installation_test_binary(
136 &self,
137 container_dir: PathBuf,
138 ) -> Option<LanguageServerBinary> {
139 let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
140 .await
141 .map(|(mut binary, ts_path)| {
142 binary.arguments = vec!["--help".into()];
143 (binary, ts_path)
144 })?;
145 *self.typescript_install_path.lock() = Some(ts_path);
146 Some(server)
147 }
148
149 async fn label_for_completion(
150 &self,
151 item: &lsp::CompletionItem,
152 language: &Arc<language::Language>,
153 ) -> Option<language::CodeLabel> {
154 use lsp::CompletionItemKind as Kind;
155 let len = item.label.len();
156 let grammar = language.grammar()?;
157 let highlight_id = match item.kind? {
158 Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
159 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
160 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
161 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
162 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
163 Kind::VARIABLE => grammar.highlight_id_for_name("type"),
164 Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
165 Kind::VALUE => grammar.highlight_id_for_name("tag"),
166 _ => None,
167 }?;
168
169 let text = match &item.detail {
170 Some(detail) => format!("{} {}", item.label, detail),
171 None => item.label.clone(),
172 };
173
174 Some(language::CodeLabel {
175 text,
176 runs: vec![(0..len, highlight_id)],
177 filter_range: 0..len,
178 })
179 }
180}
181
182fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
183 vec![server_path.into(), "--stdio".into()]
184}
185
186type TypescriptPath = PathBuf;
187async fn get_cached_server_binary(
188 container_dir: PathBuf,
189 node: Arc<dyn NodeRuntime>,
190) -> Option<(LanguageServerBinary, TypescriptPath)> {
191 (|| async move {
192 let mut last_version_dir = None;
193 let mut entries = fs::read_dir(&container_dir).await?;
194 while let Some(entry) = entries.next().await {
195 let entry = entry?;
196 if entry.file_type().await?.is_dir() {
197 last_version_dir = Some(entry.path());
198 }
199 }
200 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
201 let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
202 let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
203 if server_path.exists() && typescript_path.exists() {
204 Ok((
205 LanguageServerBinary {
206 path: node.binary_path().await?,
207 arguments: vue_server_binary_arguments(&server_path),
208 },
209 typescript_path,
210 ))
211 } else {
212 Err(anyhow!(
213 "missing executable in directory {:?}",
214 last_version_dir
215 ))
216 }
217 })()
218 .await
219 .log_err()
220}