1use super::node_runtime::NodeRuntime;
2use anyhow::{anyhow, Context, Result};
3use async_trait::async_trait;
4use futures::StreamExt;
5use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
6use serde_json::json;
7use smol::fs;
8use std::{
9 any::Any,
10 ffi::OsString,
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14use util::fs::remove_matching;
15use util::http::HttpClient;
16use util::ResultExt;
17
18fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
19 vec![
20 server_path.into(),
21 "--stdio".into(),
22 "--tsserver-path".into(),
23 "node_modules/typescript/lib".into(),
24 ]
25}
26
27pub struct TypeScriptLspAdapter {
28 node: Arc<NodeRuntime>,
29}
30
31impl TypeScriptLspAdapter {
32 const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
33 const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
34
35 pub fn new(node: Arc<NodeRuntime>) -> Self {
36 TypeScriptLspAdapter { node }
37 }
38}
39
40struct Versions {
41 typescript_version: String,
42 server_version: String,
43}
44
45#[async_trait]
46impl LspAdapter for TypeScriptLspAdapter {
47 async fn name(&self) -> LanguageServerName {
48 LanguageServerName("typescript-language-server".into())
49 }
50
51 async fn fetch_latest_server_version(
52 &self,
53 _: Arc<dyn HttpClient>,
54 ) -> Result<Box<dyn 'static + Send + Any>> {
55 Ok(Box::new(Versions {
56 typescript_version: self.node.npm_package_latest_version("typescript").await?,
57 server_version: self
58 .node
59 .npm_package_latest_version("typescript-language-server")
60 .await?,
61 }) as Box<_>)
62 }
63
64 async fn fetch_server_binary(
65 &self,
66 versions: Box<dyn 'static + Send + Any>,
67 _: Arc<dyn HttpClient>,
68 container_dir: PathBuf,
69 ) -> Result<LanguageServerBinary> {
70 let versions = versions.downcast::<Versions>().unwrap();
71 let version_dir = container_dir.join(&format!(
72 "typescript-{}:server-{}",
73 versions.typescript_version, versions.server_version
74 ));
75 fs::create_dir_all(&version_dir)
76 .await
77 .context("failed to create version directory")?;
78 let server_path = version_dir.join(Self::NEW_SERVER_PATH);
79
80 if fs::metadata(&server_path).await.is_err() {
81 self.node
82 .npm_install_packages(
83 [
84 ("typescript", versions.typescript_version.as_str()),
85 (
86 "typescript-language-server",
87 versions.server_version.as_str(),
88 ),
89 ],
90 &version_dir,
91 )
92 .await?;
93
94 remove_matching(&container_dir, |entry| entry != version_dir).await;
95 }
96
97 Ok(LanguageServerBinary {
98 path: self.node.binary_path().await?,
99 arguments: server_binary_arguments(&server_path),
100 })
101 }
102
103 async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
104 (|| async move {
105 let mut last_version_dir = None;
106 let mut entries = fs::read_dir(&container_dir).await?;
107 while let Some(entry) = entries.next().await {
108 let entry = entry?;
109 if entry.file_type().await?.is_dir() {
110 last_version_dir = Some(entry.path());
111 }
112 }
113 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
114 let old_server_path = last_version_dir.join(Self::OLD_SERVER_PATH);
115 let new_server_path = last_version_dir.join(Self::NEW_SERVER_PATH);
116 if new_server_path.exists() {
117 Ok(LanguageServerBinary {
118 path: self.node.binary_path().await?,
119 arguments: server_binary_arguments(&new_server_path),
120 })
121 } else if old_server_path.exists() {
122 Ok(LanguageServerBinary {
123 path: self.node.binary_path().await?,
124 arguments: server_binary_arguments(&old_server_path),
125 })
126 } else {
127 Err(anyhow!(
128 "missing executable in directory {:?}",
129 last_version_dir
130 ))
131 }
132 })()
133 .await
134 .log_err()
135 }
136
137 async fn label_for_completion(
138 &self,
139 item: &lsp::CompletionItem,
140 language: &Arc<language::Language>,
141 ) -> Option<language::CodeLabel> {
142 use lsp::CompletionItemKind as Kind;
143 let len = item.label.len();
144 let grammar = language.grammar()?;
145 let highlight_id = match item.kind? {
146 Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
147 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
148 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
149 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
150 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
151 _ => None,
152 }?;
153
154 let text = match &item.detail {
155 Some(detail) => format!("{} {}", item.label, detail),
156 None => item.label.clone(),
157 };
158
159 Some(language::CodeLabel {
160 text,
161 runs: vec![(0..len, highlight_id)],
162 filter_range: 0..len,
163 })
164 }
165
166 async fn initialization_options(&self) -> Option<serde_json::Value> {
167 Some(json!({
168 "provideFormatter": true
169 }))
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use gpui::TestAppContext;
176 use unindent::Unindent;
177
178 #[gpui::test]
179 async fn test_outline(cx: &mut TestAppContext) {
180 let language = crate::languages::language(
181 "typescript",
182 tree_sitter_typescript::language_typescript(),
183 None,
184 )
185 .await;
186
187 let text = r#"
188 function a() {
189 // local variables are omitted
190 let a1 = 1;
191 // all functions are included
192 async function a2() {}
193 }
194 // top-level variables are included
195 let b: C
196 function getB() {}
197 // exported variables are included
198 export const d = e;
199 "#
200 .unindent();
201
202 let buffer =
203 cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
204 let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
205 assert_eq!(
206 outline
207 .items
208 .iter()
209 .map(|item| (item.text.as_str(), item.depth))
210 .collect::<Vec<_>>(),
211 &[
212 ("function a ( )", 0),
213 ("async function a2 ( )", 1),
214 ("let b", 0),
215 ("function getB ( )", 0),
216 ("const d", 0),
217 ]
218 );
219 }
220}