1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
5use lsp::CodeActionKind;
6use node_runtime::NodeRuntime;
7use serde_json::json;
8use smol::fs;
9use std::{
10 any::Any,
11 ffi::OsString,
12 path::{Path, PathBuf},
13 sync::Arc,
14};
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 server_path = container_dir.join(Self::NEW_SERVER_PATH);
72
73 if fs::metadata(&server_path).await.is_err() {
74 self.node
75 .npm_install_packages(
76 [
77 ("typescript", versions.typescript_version.as_str()),
78 (
79 "typescript-language-server",
80 versions.server_version.as_str(),
81 ),
82 ],
83 &container_dir,
84 )
85 .await?;
86 }
87
88 Ok(LanguageServerBinary {
89 path: self.node.binary_path().await?,
90 arguments: server_binary_arguments(&server_path),
91 })
92 }
93
94 async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
95 (|| async move {
96 let mut last_version_dir = None;
97 let mut entries = fs::read_dir(&container_dir).await?;
98 while let Some(entry) = entries.next().await {
99 let entry = entry?;
100 if entry.file_type().await?.is_dir() {
101 last_version_dir = Some(entry.path());
102 }
103 }
104 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
105 let old_server_path = last_version_dir.join(Self::OLD_SERVER_PATH);
106 let new_server_path = last_version_dir.join(Self::NEW_SERVER_PATH);
107 if new_server_path.exists() {
108 Ok(LanguageServerBinary {
109 path: self.node.binary_path().await?,
110 arguments: server_binary_arguments(&new_server_path),
111 })
112 } else if old_server_path.exists() {
113 Ok(LanguageServerBinary {
114 path: self.node.binary_path().await?,
115 arguments: server_binary_arguments(&old_server_path),
116 })
117 } else {
118 Err(anyhow!(
119 "missing executable in directory {:?}",
120 last_version_dir
121 ))
122 }
123 })()
124 .await
125 .log_err()
126 }
127
128 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
129 Some(vec![
130 CodeActionKind::QUICKFIX,
131 CodeActionKind::REFACTOR,
132 CodeActionKind::REFACTOR_EXTRACT,
133 CodeActionKind::SOURCE,
134 ])
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}