WIP

Max Brunsfeld created

Change summary

crates/zed/src/languages.rs                        | 125 +++++
crates/zed/src/languages/c.rs                      | 136 +++++
crates/zed/src/languages/c/brackets.scm            |   0 
crates/zed/src/languages/c/config.toml             |   0 
crates/zed/src/languages/c/highlights.scm          |   0 
crates/zed/src/languages/c/indents.scm             |   0 
crates/zed/src/languages/c/outline.scm             |   0 
crates/zed/src/languages/json.rs                   | 131 +++++
crates/zed/src/languages/json/brackets.scm         |   0 
crates/zed/src/languages/json/config.toml          |   0 
crates/zed/src/languages/json/highlights.scm       |   0 
crates/zed/src/languages/json/indents.scm          |   0 
crates/zed/src/languages/json/outline.scm          |   0 
crates/zed/src/languages/markdown/config.toml      |   0 
crates/zed/src/languages/markdown/highlights.scm   |   0 
crates/zed/src/languages/rust.rs                   | 370 ---------------
crates/zed/src/languages/rust/brackets.scm         |   0 
crates/zed/src/languages/rust/config.toml          |   0 
crates/zed/src/languages/rust/highlights.scm       |   0 
crates/zed/src/languages/rust/indents.scm          |   0 
crates/zed/src/languages/rust/outline.scm          |   0 
crates/zed/src/languages/tsx/brackets.scm          |   0 
crates/zed/src/languages/tsx/config.toml           |   0 
crates/zed/src/languages/tsx/highlights-jsx.scm    |   0 
crates/zed/src/languages/tsx/highlights.scm        |   0 
crates/zed/src/languages/tsx/indents.scm           |   0 
crates/zed/src/languages/tsx/outline.scm           |   0 
crates/zed/src/languages/typescript.rs             | 121 +++++
crates/zed/src/languages/typescript/brackets.scm   |   0 
crates/zed/src/languages/typescript/config.toml    |   0 
crates/zed/src/languages/typescript/highlights.scm |   0 
crates/zed/src/languages/typescript/indents.scm    |   0 
crates/zed/src/languages/typescript/outline.scm    |   0 
crates/zed/src/main.rs                             |   6 
crates/zed/src/zed.rs                              |   6 
35 files changed, 523 insertions(+), 372 deletions(-)

Detailed changes

crates/zed/src/languages.rs 🔗

@@ -0,0 +1,125 @@
+use client::http::{self, HttpClient, Method};
+use gpui::Task;
+pub use language::*;
+use rust_embed::RustEmbed;
+use serde::Deserialize;
+use std::{borrow::Cow, str, sync::Arc};
+
+mod c;
+mod json;
+mod rust;
+mod typescript;
+
+#[derive(RustEmbed)]
+#[folder = "src/languages"]
+#[exclude = "*.rs"]
+struct LanguageDir;
+
+#[derive(Deserialize)]
+struct GithubRelease {
+    name: String,
+    assets: Vec<GithubReleaseAsset>,
+}
+
+#[derive(Deserialize)]
+struct GithubReleaseAsset {
+    name: String,
+    browser_download_url: http::Url,
+}
+
+pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry {
+    let languages = LanguageRegistry::new(login_shell_env_loaded);
+    for (name, grammar, lsp_adapter) in [
+        (
+            "c",
+            tree_sitter_c::language(),
+            Some(Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>),
+        ),
+        (
+            "json",
+            tree_sitter_json::language(),
+            Some(Arc::new(json::JsonLspAdapter)),
+        ),
+        (
+            "markdown",
+            tree_sitter_markdown::language(),
+            None, //
+        ),
+        (
+            "rust",
+            tree_sitter_rust::language(),
+            Some(Arc::new(rust::RustLspAdapter)),
+        ),
+        (
+            "tsx",
+            tree_sitter_typescript::language_tsx(),
+            None, //
+        ),
+        (
+            "typescript",
+            tree_sitter_typescript::language_typescript(),
+            Some(Arc::new(typescript::TypeScriptLspAdapter)),
+        ),
+    ] {
+        languages.add(Arc::new(language(name, grammar, lsp_adapter)));
+    }
+    languages
+}
+
+fn language(
+    name: &str,
+    grammar: tree_sitter::Language,
+    lsp_adapter: Option<Arc<dyn LspAdapter>>,
+) -> Language {
+    let config = toml::from_slice(
+        &LanguageDir::get(&format!("{}/config.toml", name))
+            .unwrap()
+            .data,
+    )
+    .unwrap();
+    let mut language = Language::new(config, Some(grammar));
+
+    if let Some(query) = load_query(name, "/highlights") {
+        language = language
+            .with_highlights_query(query.as_ref())
+            .expect("failed to evaluate highlights query");
+    }
+    if let Some(query) = load_query(name, "/brackets") {
+        language = language
+            .with_brackets_query(query.as_ref())
+            .expect("failed to load brackets query");
+    }
+    if let Some(query) = load_query(name, "/indents") {
+        language = language
+            .with_indents_query(query.as_ref())
+            .expect("failed to load indents query");
+    }
+    if let Some(query) = load_query(name, "/outline") {
+        language = language
+            .with_outline_query(query.as_ref())
+            .expect("failed to load outline query");
+    }
+    if let Some(lsp_adapter) = lsp_adapter {
+        language = language.with_lsp_adapter(lsp_adapter)
+    }
+    language
+}
+
+fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
+    let mut result = None;
+    for path in LanguageDir::iter() {
+        if let Some(remainder) = path.strip_prefix(name) {
+            if remainder.starts_with(filename_prefix) {
+                let contents = match LanguageDir::get(path.as_ref()).unwrap().data {
+                    Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+                    Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
+                };
+                match &mut result {
+                    None => result = Some(contents),
+                    Some(r) => r.to_mut().push_str(contents.as_ref()),
+                }
+            }
+        }
+    }
+    result
+}

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

@@ -0,0 +1,136 @@
+use anyhow::{anyhow, Result};
+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 util::{ResultExt, TryFutureExt};
+
+use super::GithubRelease;
+
+pub struct CLspAdapter;
+
+impl super::LspAdapter for CLspAdapter {
+    fn name(&self) -> &'static str {
+        "clangd"
+    }
+
+    fn fetch_latest_server_version(
+        &self,
+        http: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+        async move {
+            let release = http
+                .send(
+                    surf::RequestBuilder::new(
+                        Method::Get,
+                        http::Url::parse(
+                            "https://api.github.com/repos/clangd/clangd/releases/latest",
+                        )
+                        .unwrap(),
+                    )
+                    .middleware(surf::middleware::Redirect::default())
+                    .build(),
+                )
+                .await
+                .map_err(|err| anyhow!("error fetching latest release: {}", err))?
+                .body_json::<GithubRelease>()
+                .await
+                .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
+            let asset_name = format!("clangd-mac-{}.zip", release.name);
+            let asset = release
+                .assets
+                .iter()
+                .find(|asset| asset.name == asset_name)
+                .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
+            Ok(LspBinaryVersion {
+                name: release.name,
+                url: Some(asset.browser_download_url.clone()),
+            })
+        }
+        .boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        version: LspBinaryVersion,
+        http: Arc<dyn HttpClient>,
+        container_dir: PathBuf,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        async move {
+            let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
+            let version_dir = container_dir.join(format!("clangd_{}", version.name));
+            let binary_path = version_dir.join("bin/clangd");
+
+            if fs::metadata(&binary_path).await.is_err() {
+                let response = http
+                    .send(
+                        surf::RequestBuilder::new(Method::Get, version.url.unwrap())
+                            .middleware(surf::middleware::Redirect::default())
+                            .build(),
+                    )
+                    .await
+                    .map_err(|err| anyhow!("error downloading release: {}", err))?;
+                let mut file = File::create(&zip_path).await?;
+                if !response.status().is_success() {
+                    Err(anyhow!(
+                        "download failed with status {}",
+                        response.status().to_string()
+                    ))?;
+                }
+                futures::io::copy(response, &mut file).await?;
+
+                let unzip_status = smol::process::Command::new("unzip")
+                    .current_dir(&container_dir)
+                    .arg(&zip_path)
+                    .output()
+                    .await?
+                    .status;
+                if !unzip_status.success() {
+                    Err(anyhow!("failed to unzip clangd archive"))?;
+                }
+
+                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != version_dir {
+                                fs::remove_dir_all(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(binary_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let mut last_clangd_dir = None;
+            let mut entries = fs::read_dir(&container_dir).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_clangd_dir = Some(entry.path());
+                }
+            }
+            let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let clangd_bin = clangd_dir.join("bin/clangd");
+            if clangd_bin.exists() {
+                Ok(clangd_bin)
+            } else {
+                Err(anyhow!(
+                    "missing clangd binary in directory {:?}",
+                    clangd_dir
+                ))
+            }
+        }
+        .log_err()
+        .boxed()
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+}

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

@@ -0,0 +1,131 @@
+use anyhow::{anyhow, Context, Result};
+use client::http::HttpClient;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use language::{LspAdapter, LspBinaryVersion};
+use serde::Deserialize;
+use serde_json::json;
+use smol::fs;
+use std::{path::PathBuf, sync::Arc};
+use util::ResultExt;
+
+pub struct JsonLspAdapter;
+
+impl JsonLspAdapter {
+    const BIN_PATH: &'static str =
+        "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
+}
+
+impl LspAdapter for JsonLspAdapter {
+    fn name(&self) -> &'static str {
+        "vscode-json-languageserver"
+    }
+
+    fn server_args(&self) -> &[&str] {
+        &["--stdio"]
+    }
+
+    fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+        async move {
+            #[derive(Deserialize)]
+            struct NpmInfo {
+                versions: Vec<String>,
+            }
+
+            let output = smol::process::Command::new("npm")
+                .args(["info", "vscode-json-languageserver", "--json"])
+                .output()
+                .await?;
+            if !output.status.success() {
+                Err(anyhow!("failed to execute npm info"))?;
+            }
+            let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
+
+            Ok(LspBinaryVersion {
+                name: info
+                    .versions
+                    .pop()
+                    .ok_or_else(|| anyhow!("no versions found in npm info"))?,
+                url: Default::default(),
+            })
+        }
+        .boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        version: LspBinaryVersion,
+        _: Arc<dyn HttpClient>,
+        container_dir: PathBuf,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        async move {
+            let version_dir = container_dir.join(&version.name);
+            fs::create_dir_all(&version_dir)
+                .await
+                .context("failed to create version directory")?;
+            let binary_path = version_dir.join(Self::BIN_PATH);
+
+            if fs::metadata(&binary_path).await.is_err() {
+                let output = smol::process::Command::new("npm")
+                    .current_dir(&version_dir)
+                    .arg("install")
+                    .arg(format!("vscode-json-languageserver@{}", version.name))
+                    .output()
+                    .await
+                    .context("failed to run npm install")?;
+                if !output.status.success() {
+                    Err(anyhow!("failed to install vscode-json-languageserver"))?;
+                }
+
+                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != version_dir {
+                                fs::remove_dir_all(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(binary_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let mut last_version_dir = None;
+            let mut entries = fs::read_dir(&container_dir).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_version_dir = Some(entry.path());
+                }
+            }
+            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let bin_path = last_version_dir.join(Self::BIN_PATH);
+            if bin_path.exists() {
+                Ok(bin_path)
+            } else {
+                Err(anyhow!(
+                    "missing executable in directory {:?}",
+                    last_version_dir
+                ))
+            }
+        }
+        .log_err()
+        .boxed()
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+
+    fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}

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

@@ -1,37 +1,17 @@
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use client::http::{self, HttpClient, Method};
 use futures::{future::BoxFuture, FutureExt, StreamExt};
-use gpui::Task;
 pub use language::*;
 use lazy_static::lazy_static;
 use regex::Regex;
-use rust_embed::RustEmbed;
-use serde::Deserialize;
-use serde_json::json;
 use smol::fs::{self, File};
 use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
 use util::{ResultExt, TryFutureExt};
 
-#[derive(RustEmbed)]
-#[folder = "languages"]
-struct LanguageDir;
+use super::GithubRelease;
 
-struct RustLspAdapter;
-struct CLspAdapter;
-struct JsonLspAdapter;
-
-#[derive(Deserialize)]
-struct GithubRelease {
-    name: String,
-    assets: Vec<GithubReleaseAsset>,
-}
-
-#[derive(Deserialize)]
-struct GithubReleaseAsset {
-    name: String,
-    browser_download_url: http::Url,
-}
+pub struct RustLspAdapter;
 
 impl LspAdapter for RustLspAdapter {
     fn name(&self) -> &'static str {
@@ -287,353 +267,11 @@ impl LspAdapter for RustLspAdapter {
     }
 }
 
-impl LspAdapter for CLspAdapter {
-    fn name(&self) -> &'static str {
-        "clangd"
-    }
-
-    fn fetch_latest_server_version(
-        &self,
-        http: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
-        async move {
-            let release = http
-                .send(
-                    surf::RequestBuilder::new(
-                        Method::Get,
-                        http::Url::parse(
-                            "https://api.github.com/repos/clangd/clangd/releases/latest",
-                        )
-                        .unwrap(),
-                    )
-                    .middleware(surf::middleware::Redirect::default())
-                    .build(),
-                )
-                .await
-                .map_err(|err| anyhow!("error fetching latest release: {}", err))?
-                .body_json::<GithubRelease>()
-                .await
-                .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
-            let asset_name = format!("clangd-mac-{}.zip", release.name);
-            let asset = release
-                .assets
-                .iter()
-                .find(|asset| asset.name == asset_name)
-                .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
-            Ok(LspBinaryVersion {
-                name: release.name,
-                url: Some(asset.browser_download_url.clone()),
-            })
-        }
-        .boxed()
-    }
-
-    fn fetch_server_binary(
-        &self,
-        version: LspBinaryVersion,
-        http: Arc<dyn HttpClient>,
-        container_dir: PathBuf,
-    ) -> BoxFuture<'static, Result<PathBuf>> {
-        async move {
-            let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
-            let version_dir = container_dir.join(format!("clangd_{}", version.name));
-            let binary_path = version_dir.join("bin/clangd");
-
-            if fs::metadata(&binary_path).await.is_err() {
-                let response = http
-                    .send(
-                        surf::RequestBuilder::new(Method::Get, version.url.unwrap())
-                            .middleware(surf::middleware::Redirect::default())
-                            .build(),
-                    )
-                    .await
-                    .map_err(|err| anyhow!("error downloading release: {}", err))?;
-                let mut file = File::create(&zip_path).await?;
-                if !response.status().is_success() {
-                    Err(anyhow!(
-                        "download failed with status {}",
-                        response.status().to_string()
-                    ))?;
-                }
-                futures::io::copy(response, &mut file).await?;
-
-                let unzip_status = smol::process::Command::new("unzip")
-                    .current_dir(&container_dir)
-                    .arg(&zip_path)
-                    .output()
-                    .await?
-                    .status;
-                if !unzip_status.success() {
-                    Err(anyhow!("failed to unzip clangd archive"))?;
-                }
-
-                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                    while let Some(entry) = entries.next().await {
-                        if let Some(entry) = entry.log_err() {
-                            let entry_path = entry.path();
-                            if entry_path.as_path() != version_dir {
-                                fs::remove_dir_all(&entry_path).await.log_err();
-                            }
-                        }
-                    }
-                }
-            }
-
-            Ok(binary_path)
-        }
-        .boxed()
-    }
-
-    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
-        async move {
-            let mut last_clangd_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_clangd_dir = Some(entry.path());
-                }
-            }
-            let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let clangd_bin = clangd_dir.join("bin/clangd");
-            if clangd_bin.exists() {
-                Ok(clangd_bin)
-            } else {
-                Err(anyhow!(
-                    "missing clangd binary in directory {:?}",
-                    clangd_dir
-                ))
-            }
-        }
-        .log_err()
-        .boxed()
-    }
-
-    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
-}
-
-impl JsonLspAdapter {
-    const BIN_PATH: &'static str =
-        "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
-}
-
-impl LspAdapter for JsonLspAdapter {
-    fn name(&self) -> &'static str {
-        "vscode-json-languageserver"
-    }
-
-    fn server_args(&self) -> &[&str] {
-        &["--stdio"]
-    }
-
-    fn fetch_latest_server_version(
-        &self,
-        _: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
-        async move {
-            #[derive(Deserialize)]
-            struct NpmInfo {
-                versions: Vec<String>,
-            }
-
-            let output = smol::process::Command::new("npm")
-                .args(["info", "vscode-json-languageserver", "--json"])
-                .output()
-                .await?;
-            if !output.status.success() {
-                Err(anyhow!("failed to execute npm info"))?;
-            }
-            let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
-
-            Ok(LspBinaryVersion {
-                name: info
-                    .versions
-                    .pop()
-                    .ok_or_else(|| anyhow!("no versions found in npm info"))?,
-                url: Default::default(),
-            })
-        }
-        .boxed()
-    }
-
-    fn fetch_server_binary(
-        &self,
-        version: LspBinaryVersion,
-        _: Arc<dyn HttpClient>,
-        container_dir: PathBuf,
-    ) -> BoxFuture<'static, Result<PathBuf>> {
-        async move {
-            let version_dir = container_dir.join(&version.name);
-            fs::create_dir_all(&version_dir)
-                .await
-                .context("failed to create version directory")?;
-            let binary_path = version_dir.join(Self::BIN_PATH);
-
-            if fs::metadata(&binary_path).await.is_err() {
-                let output = smol::process::Command::new("npm")
-                    .current_dir(&version_dir)
-                    .arg("install")
-                    .arg(format!("vscode-json-languageserver@{}", version.name))
-                    .output()
-                    .await
-                    .context("failed to run npm install")?;
-                if !output.status.success() {
-                    Err(anyhow!("failed to install vscode-json-languageserver"))?;
-                }
-
-                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                    while let Some(entry) = entries.next().await {
-                        if let Some(entry) = entry.log_err() {
-                            let entry_path = entry.path();
-                            if entry_path.as_path() != version_dir {
-                                fs::remove_dir_all(&entry_path).await.log_err();
-                            }
-                        }
-                    }
-                }
-            }
-
-            Ok(binary_path)
-        }
-        .boxed()
-    }
-
-    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
-        async move {
-            let mut last_version_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let bin_path = last_version_dir.join(Self::BIN_PATH);
-            if bin_path.exists() {
-                Ok(bin_path)
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    last_version_dir
-                ))
-            }
-        }
-        .log_err()
-        .boxed()
-    }
-
-    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
-
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
-            "provideFormatter": true
-        }))
-    }
-}
-
-pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry {
-    let languages = LanguageRegistry::new(login_shell_env_loaded);
-    for (name, grammar, lsp_adapter) in [
-        (
-            "c",
-            tree_sitter_c::language(),
-            Some(Arc::new(CLspAdapter) as Arc<dyn LspAdapter>),
-        ),
-        (
-            "json",
-            tree_sitter_json::language(),
-            Some(Arc::new(JsonLspAdapter)),
-        ),
-        (
-            "markdown",
-            tree_sitter_markdown::language(),
-            None, //
-        ),
-        (
-            "rust",
-            tree_sitter_rust::language(),
-            Some(Arc::new(RustLspAdapter)),
-        ),
-        (
-            "tsx",
-            tree_sitter_typescript::language_tsx(),
-            None, //
-        ),
-        (
-            "typescript",
-            tree_sitter_typescript::language_typescript(),
-            None, //
-        ),
-    ] {
-        languages.add(Arc::new(language(name, grammar, lsp_adapter)));
-    }
-    languages
-}
-
-fn language(
-    name: &str,
-    grammar: tree_sitter::Language,
-    lsp_adapter: Option<Arc<dyn LspAdapter>>,
-) -> Language {
-    let config = toml::from_slice(
-        &LanguageDir::get(&format!("{}/config.toml", name))
-            .unwrap()
-            .data,
-    )
-    .unwrap();
-    let mut language = Language::new(config, Some(grammar));
-
-    if let Some(query) = load_query(name, "/highlights") {
-        language = language
-            .with_highlights_query(query.as_ref())
-            .expect("failed to evaluate highlights query");
-    }
-    if let Some(query) = load_query(name, "/brackets") {
-        language = language
-            .with_brackets_query(query.as_ref())
-            .expect("failed to load brackets query");
-    }
-    if let Some(query) = load_query(name, "/indents") {
-        language = language
-            .with_indents_query(query.as_ref())
-            .expect("failed to load indents query");
-    }
-    if let Some(query) = load_query(name, "/outline") {
-        language = language
-            .with_outline_query(query.as_ref())
-            .expect("failed to load outline query");
-    }
-    if let Some(lsp_adapter) = lsp_adapter {
-        language = language.with_lsp_adapter(lsp_adapter)
-    }
-    language
-}
-
-fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
-    let mut result = None;
-    for path in LanguageDir::iter() {
-        if let Some(remainder) = path.strip_prefix(name) {
-            if remainder.starts_with(filename_prefix) {
-                let contents = match LanguageDir::get(path.as_ref()).unwrap().data {
-                    Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
-                    Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
-                };
-                match &mut result {
-                    None => result = Some(contents),
-                    Some(r) => r.to_mut().push_str(contents.as_ref()),
-                }
-            }
-        }
-    }
-    result
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::languages::{language, LspAdapter};
     use gpui::color::Color;
-    use language::LspAdapter;
     use theme::SyntaxTheme;
 
     #[test]

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

@@ -0,0 +1,121 @@
+pub struct TypeScriptLspAdapter;
+
+impl TypeScriptLspAdapter {
+    const BIN_PATH: &'static str =
+        "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
+}
+
+impl super::LspAdapter for TypeScriptLspAdapter {
+    fn name(&self) -> &'static str {
+        "typescript-language-server"
+    }
+
+    fn server_args(&self) -> &[&str] {
+        &["--stdio"]
+    }
+
+    fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+        async move {
+            #[derive(Deserialize)]
+            struct NpmInfo {
+                versions: Vec<String>,
+            }
+
+            let output = smol::process::Command::new("npm")
+                .args(["info", "vscode-json-languageserver", "--json"])
+                .output()
+                .await?;
+            if !output.status.success() {
+                Err(anyhow!("failed to execute npm info"))?;
+            }
+            let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
+
+            Ok(LspBinaryVersion {
+                name: info
+                    .versions
+                    .pop()
+                    .ok_or_else(|| anyhow!("no versions found in npm info"))?,
+                url: Default::default(),
+            })
+        }
+        .boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        version: LspBinaryVersion,
+        _: Arc<dyn HttpClient>,
+        container_dir: PathBuf,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        async move {
+            let version_dir = container_dir.join(&version.name);
+            fs::create_dir_all(&version_dir)
+                .await
+                .context("failed to create version directory")?;
+            let binary_path = version_dir.join(Self::BIN_PATH);
+
+            if fs::metadata(&binary_path).await.is_err() {
+                let output = smol::process::Command::new("npm")
+                    .current_dir(&version_dir)
+                    .arg("install")
+                    .arg(format!("vscode-json-languageserver@{}", version.name))
+                    .output()
+                    .await
+                    .context("failed to run npm install")?;
+                if !output.status.success() {
+                    Err(anyhow!("failed to install vscode-json-languageserver"))?;
+                }
+
+                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != version_dir {
+                                fs::remove_dir_all(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(binary_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let mut last_version_dir = None;
+            let mut entries = fs::read_dir(&container_dir).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_version_dir = Some(entry.path());
+                }
+            }
+            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let bin_path = last_version_dir.join(Self::BIN_PATH);
+            if bin_path.exists() {
+                Ok(bin_path)
+            } else {
+                Err(anyhow!(
+                    "missing executable in directory {:?}",
+                    last_version_dir
+                ))
+            }
+        }
+        .log_err()
+        .boxed()
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+
+    fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}

crates/zed/src/main.rs 🔗

@@ -19,7 +19,7 @@ use workspace::{
     AppState, OpenNew, OpenParams, OpenPaths, Settings,
 };
 use zed::{
-    self, assets::Assets, build_window_options, build_workspace, fs::RealFs, language, menus,
+    self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus,
 };
 
 fn main() {
@@ -34,7 +34,7 @@ fn main() {
     let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme)
         .unwrap()
         .with_overrides(
-            language::PLAIN_TEXT.name(),
+            languages::PLAIN_TEXT.name(),
             settings::LanguageOverride {
                 soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
                 ..Default::default()
@@ -60,7 +60,7 @@ fn main() {
     app.run(move |cx| {
         let http = http::client();
         let client = client::Client::new(http.clone());
-        let mut languages = language::build_language_registry(login_shell_env_loaded);
+        let mut languages = languages::build_language_registry(login_shell_env_loaded);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         let channel_list =
             cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));

crates/zed/src/zed.rs 🔗

@@ -1,5 +1,5 @@
 pub mod assets;
-pub mod language;
+pub mod languages;
 pub mod menus;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
@@ -557,7 +557,7 @@ mod tests {
             assert_eq!(editor.title(cx), "untitled");
             assert!(Arc::ptr_eq(
                 editor.language(cx).unwrap(),
-                &language::PLAIN_TEXT
+                &languages::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
             assert!(editor.is_dirty(cx));
@@ -647,7 +647,7 @@ mod tests {
         editor.update(cx, |editor, cx| {
             assert!(Arc::ptr_eq(
                 editor.language(cx).unwrap(),
-                &language::PLAIN_TEXT
+                &languages::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
             assert!(editor.is_dirty(cx.as_ref()));