Add typescript language server

Keith Simmons and Max Brunsfeld created

Currently not tested for tsx files

Co-authored-by: Max Brunsfeld <max@zed.dev>

Change summary

crates/language/src/language.rs        |  9 ++-
crates/project/src/project.rs          |  8 ++-
crates/zed/src/languages.rs            |  4 
crates/zed/src/languages/c.rs          | 15 +++--
crates/zed/src/languages/json.rs       | 23 ++++----
crates/zed/src/languages/rust.rs       | 15 +++--
crates/zed/src/languages/typescript.rs | 69 ++++++++++++++++++++-------
7 files changed, 90 insertions(+), 53 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -20,6 +20,7 @@ use parking_lot::{Mutex, RwLock};
 use serde::Deserialize;
 use serde_json::Value;
 use std::{
+    any::Any,
     cell::RefCell,
     ops::Range,
     path::{Path, PathBuf},
@@ -61,9 +62,9 @@ pub trait ToLspPosition {
     fn to_lsp_position(self) -> lsp::Position;
 }
 
-pub struct LspBinaryVersion {
+pub struct GitHubLspBinaryVersion {
     pub name: String,
-    pub url: Option<http::Url>,
+    pub url: http::Url,
 }
 
 pub trait LspAdapter: 'static + Send + Sync {
@@ -71,10 +72,10 @@ pub trait LspAdapter: 'static + Send + Sync {
     fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>>;
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>>;
     fn fetch_server_binary(
         &self,
-        version: LspBinaryVersion,
+        version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> BoxFuture<'static, Result<PathBuf>>;

crates/project/src/project.rs 🔗

@@ -2278,11 +2278,12 @@ impl Project {
                     Ok(completions
                         .into_iter()
                         .filter_map(|lsp_completion| {
-                            let (old_range, new_text) = match lsp_completion.text_edit.as_ref()? {
-                                lsp::CompletionTextEdit::Edit(edit) => {
+                            let (old_range, new_text) = match lsp_completion.text_edit.as_ref() {
+                                Some(lsp::CompletionTextEdit::Edit(edit)) => {
                                     (range_from_lsp(edit.range), edit.new_text.clone())
                                 }
-                                lsp::CompletionTextEdit::InsertAndReplace(_) => {
+                                None => (position..position, lsp_completion.label.clone()),
+                                Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
                                     log::info!("unsupported insert/replace completion");
                                     return None;
                                 }
@@ -2307,6 +2308,7 @@ impl Project {
                                     lsp_completion,
                                 })
                             } else {
+                                log::info!("completion out of expected range");
                                 None
                             }
                         })

crates/zed/src/languages.rs 🔗

@@ -1,4 +1,4 @@
-use client::http::{self, HttpClient, Method};
+use client::http;
 use gpui::Task;
 pub use language::*;
 use rust_embed::RustEmbed;
@@ -53,7 +53,7 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
         (
             "tsx",
             tree_sitter_typescript::language_tsx(),
-            None, //
+            Some(Arc::new(typescript::TypeScriptLspAdapter)),
         ),
         (
             "typescript",

crates/zed/src/languages/c.rs 🔗

@@ -3,7 +3,7 @@ use client::http::{self, HttpClient, Method};
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 pub use language::*;
 use smol::fs::{self, File};
-use std::{path::PathBuf, str, sync::Arc};
+use std::{any::Any, path::PathBuf, str, sync::Arc};
 use util::{ResultExt, TryFutureExt};
 
 use super::GithubRelease;
@@ -18,7 +18,7 @@ impl super::LspAdapter for CLspAdapter {
     fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
         async move {
             let release = http
                 .send(
@@ -43,20 +43,21 @@ impl super::LspAdapter for CLspAdapter {
                 .iter()
                 .find(|asset| asset.name == asset_name)
                 .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
-            Ok(LspBinaryVersion {
+            Ok(Box::new(GitHubLspBinaryVersion {
                 name: release.name,
-                url: Some(asset.browser_download_url.clone()),
-            })
+                url: asset.browser_download_url.clone(),
+            }) as Box<_>)
         }
         .boxed()
     }
 
     fn fetch_server_binary(
         &self,
-        version: LspBinaryVersion,
+        version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> BoxFuture<'static, Result<PathBuf>> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         async move {
             let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
             let version_dir = container_dir.join(format!("clangd_{}", version.name));
@@ -65,7 +66,7 @@ impl super::LspAdapter for CLspAdapter {
             if fs::metadata(&binary_path).await.is_err() {
                 let response = http
                     .send(
-                        surf::RequestBuilder::new(Method::Get, version.url.unwrap())
+                        surf::RequestBuilder::new(Method::Get, version.url)
                             .middleware(surf::middleware::Redirect::default())
                             .build(),
                     )

crates/zed/src/languages/json.rs 🔗

@@ -1,12 +1,12 @@
 use anyhow::{anyhow, Context, Result};
 use client::http::HttpClient;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
-use language::{LspAdapter, LspBinaryVersion};
+use language::LspAdapter;
 use serde::Deserialize;
 use serde_json::json;
 use smol::fs;
-use std::{path::PathBuf, sync::Arc};
-use util::ResultExt;
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::{ResultExt, TryFutureExt};
 
 pub struct JsonLspAdapter;
 
@@ -27,7 +27,7 @@ impl LspAdapter for JsonLspAdapter {
     fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Any + Send>>> {
         async move {
             #[derive(Deserialize)]
             struct NpmInfo {
@@ -43,25 +43,24 @@ impl LspAdapter for JsonLspAdapter {
             }
             let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
 
-            Ok(LspBinaryVersion {
-                name: info
-                    .versions
+            Ok(Box::new(
+                info.versions
                     .pop()
                     .ok_or_else(|| anyhow!("no versions found in npm info"))?,
-                url: Default::default(),
-            })
+            ) as Box<_>)
         }
         .boxed()
     }
 
     fn fetch_server_binary(
         &self,
-        version: LspBinaryVersion,
+        version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> BoxFuture<'static, Result<PathBuf>> {
+        let version = version.downcast::<String>().unwrap();
         async move {
-            let version_dir = container_dir.join(&version.name);
+            let version_dir = container_dir.join(version.as_str());
             fs::create_dir_all(&version_dir)
                 .await
                 .context("failed to create version directory")?;
@@ -71,7 +70,7 @@ impl LspAdapter for JsonLspAdapter {
                 let output = smol::process::Command::new("npm")
                     .current_dir(&version_dir)
                     .arg("install")
-                    .arg(format!("vscode-json-languageserver@{}", version.name))
+                    .arg(format!("vscode-json-languageserver@{}", version))
                     .output()
                     .await
                     .context("failed to run npm install")?;

crates/zed/src/languages/rust.rs 🔗

@@ -6,7 +6,7 @@ pub use language::*;
 use lazy_static::lazy_static;
 use regex::Regex;
 use smol::fs::{self, File};
-use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
+use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
 use util::{ResultExt, TryFutureExt};
 
 use super::GithubRelease;
@@ -21,7 +21,7 @@ impl LspAdapter for RustLspAdapter {
     fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
         async move {
             let release = http
             .send(
@@ -46,27 +46,28 @@ impl LspAdapter for RustLspAdapter {
                 .iter()
                 .find(|asset| asset.name == asset_name)
                 .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
-            Ok(LspBinaryVersion {
+            Ok(Box::new(GitHubLspBinaryVersion {
                 name: release.name,
-                url: Some(asset.browser_download_url.clone()),
-            })
+                url: asset.browser_download_url.clone(),
+            }) as Box<_>)
         }
         .boxed()
     }
 
     fn fetch_server_binary(
         &self,
-        version: LspBinaryVersion,
+        version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> BoxFuture<'static, Result<PathBuf>> {
         async move {
+            let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
             let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 
             if fs::metadata(&destination_path).await.is_err() {
                 let response = http
                     .send(
-                        surf::RequestBuilder::new(Method::Get, version.url.unwrap())
+                        surf::RequestBuilder::new(Method::Get, version.url)
                             .middleware(surf::middleware::Redirect::default())
                             .build(),
                     )

crates/zed/src/languages/typescript.rs 🔗

@@ -1,57 +1,86 @@
+use anyhow::{anyhow, Context, Result};
+use client::http::HttpClient;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use language::LspAdapter;
+use serde::Deserialize;
+use serde_json::json;
+use smol::fs;
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::{ResultExt, TryFutureExt};
+
 pub struct TypeScriptLspAdapter;
 
 impl TypeScriptLspAdapter {
-    const BIN_PATH: &'static str =
-        "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
+    const BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
+}
+
+struct Versions {
+    typescript_version: String,
+    server_version: String,
 }
 
-impl super::LspAdapter for TypeScriptLspAdapter {
+impl LspAdapter for TypeScriptLspAdapter {
     fn name(&self) -> &'static str {
         "typescript-language-server"
     }
 
     fn server_args(&self) -> &[&str] {
-        &["--stdio"]
+        &["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
     }
 
     fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
         async move {
             #[derive(Deserialize)]
             struct NpmInfo {
                 versions: Vec<String>,
             }
 
-            let output = smol::process::Command::new("npm")
-                .args(["info", "vscode-json-languageserver", "--json"])
+            let typescript_output = smol::process::Command::new("npm")
+                .args(["info", "typescript", "--json"])
+                .output()
+                .await?;
+            if !typescript_output.status.success() {
+                Err(anyhow!("failed to execute npm info"))?;
+            }
+            let mut typescript_info: NpmInfo = serde_json::from_slice(&typescript_output.stdout)?;
+
+            let server_output = smol::process::Command::new("npm")
+                .args(["info", "typescript-language-server", "--json"])
                 .output()
                 .await?;
-            if !output.status.success() {
+            if !server_output.status.success() {
                 Err(anyhow!("failed to execute npm info"))?;
             }
-            let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
+            let mut server_info: NpmInfo = serde_json::from_slice(&server_output.stdout)?;
 
-            Ok(LspBinaryVersion {
-                name: info
+            Ok(Box::new(Versions {
+                typescript_version: typescript_info
                     .versions
                     .pop()
-                    .ok_or_else(|| anyhow!("no versions found in npm info"))?,
-                url: Default::default(),
-            })
+                    .ok_or_else(|| anyhow!("no versions found in typescript npm info"))?,
+                server_version: server_info.versions.pop().ok_or_else(|| {
+                    anyhow!("no versions found in typescript language server npm info")
+                })?,
+            }) as Box<_>)
         }
         .boxed()
     }
 
     fn fetch_server_binary(
         &self,
-        version: LspBinaryVersion,
+        versions: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> BoxFuture<'static, Result<PathBuf>> {
+        let versions = versions.downcast::<Versions>().unwrap();
         async move {
-            let version_dir = container_dir.join(&version.name);
+            let version_dir = container_dir.join(&format!(
+                "typescript-{}:server-{}",
+                versions.typescript_version, versions.server_version
+            ));
             fs::create_dir_all(&version_dir)
                 .await
                 .context("failed to create version directory")?;
@@ -61,12 +90,16 @@ impl super::LspAdapter for TypeScriptLspAdapter {
                 let output = smol::process::Command::new("npm")
                     .current_dir(&version_dir)
                     .arg("install")
-                    .arg(format!("vscode-json-languageserver@{}", version.name))
+                    .arg(format!("typescript@{}", versions.typescript_version))
+                    .arg(format!(
+                        "typescript-language-server@{}",
+                        versions.server_version
+                    ))
                     .output()
                     .await
                     .context("failed to run npm install")?;
                 if !output.status.success() {
-                    Err(anyhow!("failed to install vscode-json-languageserver"))?;
+                    Err(anyhow!("failed to install typescript-language-server"))?;
                 }
 
                 if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {